xref: /aosp_15_r20/external/cronet/build/gn_helpers_unittest.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env python3
2# Copyright 2016 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import os
7import pathlib
8import shutil
9import sys
10import tempfile
11import textwrap
12import unittest
13from unittest import mock
14
15import gn_helpers
16
17
18class UnitTest(unittest.TestCase):
19  def test_ToGNString(self):
20    test_cases = [
21        (42, '42', '42'), ('foo', '"foo"', '"foo"'), (True, 'true', 'true'),
22        (False, 'false', 'false'), ('', '""', '""'),
23        ('\\$"$\\', '"\\\\\\$\\"\\$\\\\"', '"\\\\\\$\\"\\$\\\\"'),
24        (' \t\r\n', '" $0x09$0x0D$0x0A"', '" $0x09$0x0D$0x0A"'),
25        (u'\u2713', '"$0xE2$0x9C$0x93"', '"$0xE2$0x9C$0x93"'),
26        ([], '[  ]', '[]'), ([1], '[ 1 ]', '[\n  1\n]\n'),
27        ([3, 1, 4, 1], '[ 3, 1, 4, 1 ]', '[\n  3,\n  1,\n  4,\n  1\n]\n'),
28        (['a', True, 2], '[ "a", true, 2 ]', '[\n  "a",\n  true,\n  2\n]\n'),
29        ({
30            'single': 'item'
31        }, 'single = "item"\n', 'single = "item"\n'),
32        ({
33            'kEy': 137,
34            '_42A_Zaz_': [False, True]
35        }, '_42A_Zaz_ = [ false, true ]\nkEy = 137\n',
36         '_42A_Zaz_ = [\n  false,\n  true\n]\nkEy = 137\n'),
37        ([1, 'two',
38          ['"thr,.$\\', True, False, [],
39           u'(\u2713)']], '[ 1, "two", [ "\\"thr,.\\$\\\\", true, false, ' +
40         '[  ], "($0xE2$0x9C$0x93)" ] ]', '''[
41  1,
42  "two",
43  [
44    "\\"thr,.\\$\\\\",
45    true,
46    false,
47    [],
48    "($0xE2$0x9C$0x93)"
49  ]
50]
51'''),
52        ({
53            's': 'foo',
54            'n': 42,
55            'b': True,
56            'a': [3, 'x']
57        }, 'a = [ 3, "x" ]\nb = true\nn = 42\ns = "foo"\n',
58         'a = [\n  3,\n  "x"\n]\nb = true\nn = 42\ns = "foo"\n'),
59        (
60            [[[], [[]]], []],
61            '[ [ [  ], [ [  ] ] ], [  ] ]',
62            '[\n  [\n    [],\n    [\n      []\n    ]\n  ],\n  []\n]\n',
63        ),
64        (
65            [{
66                'a': 1,
67                'c': {
68                    'z': 8
69                },
70                'b': []
71            }],
72            '[ { a = 1\nb = [  ]\nc = { z = 8 } } ]\n',
73            '[\n  {\n    a = 1\n    b = []\n    c = {\n' +
74            '      z = 8\n    }\n  }\n]\n',
75        )
76    ]
77    for obj, exp_ugly, exp_pretty in test_cases:
78      out_ugly = gn_helpers.ToGNString(obj)
79      self.assertEqual(exp_ugly, out_ugly)
80      out_pretty = gn_helpers.ToGNString(obj, pretty=True)
81      self.assertEqual(exp_pretty, out_pretty)
82
83  def test_UnescapeGNString(self):
84    # Backslash followed by a \, $, or " means the folling character without
85    # the special meaning. Backslash followed by everything else is a literal.
86    self.assertEqual(
87        gn_helpers.UnescapeGNString('\\as\\$\\\\asd\\"'),
88        '\\as$\\asd"')
89
90  def test_FromGNString(self):
91    self.assertEqual(
92        gn_helpers.FromGNString('[1, -20, true, false,["as\\"", []]]'),
93        [ 1, -20, True, False, [ 'as"', [] ] ])
94
95    with self.assertRaises(gn_helpers.GNError):
96      parser = gn_helpers.GNValueParser('123 456')
97      parser.Parse()
98
99  def test_ParseBool(self):
100    parser = gn_helpers.GNValueParser('true')
101    self.assertEqual(parser.Parse(), True)
102
103    parser = gn_helpers.GNValueParser('false')
104    self.assertEqual(parser.Parse(), False)
105
106  def test_ParseNumber(self):
107    parser = gn_helpers.GNValueParser('123')
108    self.assertEqual(parser.ParseNumber(), 123)
109
110    with self.assertRaises(gn_helpers.GNError):
111      parser = gn_helpers.GNValueParser('')
112      parser.ParseNumber()
113    with self.assertRaises(gn_helpers.GNError):
114      parser = gn_helpers.GNValueParser('a123')
115      parser.ParseNumber()
116
117  def test_ParseString(self):
118    parser = gn_helpers.GNValueParser('"asdf"')
119    self.assertEqual(parser.ParseString(), 'asdf')
120
121    with self.assertRaises(gn_helpers.GNError):
122      parser = gn_helpers.GNValueParser('')  # Empty.
123      parser.ParseString()
124    with self.assertRaises(gn_helpers.GNError):
125      parser = gn_helpers.GNValueParser('asdf')  # Unquoted.
126      parser.ParseString()
127    with self.assertRaises(gn_helpers.GNError):
128      parser = gn_helpers.GNValueParser('"trailing')  # Unterminated.
129      parser.ParseString()
130
131  def test_ParseList(self):
132    parser = gn_helpers.GNValueParser('[1,]')  # Optional end comma OK.
133    self.assertEqual(parser.ParseList(), [ 1 ])
134
135    with self.assertRaises(gn_helpers.GNError):
136      parser = gn_helpers.GNValueParser('')  # Empty.
137      parser.ParseList()
138    with self.assertRaises(gn_helpers.GNError):
139      parser = gn_helpers.GNValueParser('asdf')  # No [].
140      parser.ParseList()
141    with self.assertRaises(gn_helpers.GNError):
142      parser = gn_helpers.GNValueParser('[1, 2')  # Unterminated
143      parser.ParseList()
144    with self.assertRaises(gn_helpers.GNError):
145      parser = gn_helpers.GNValueParser('[1 2]')  # No separating comma.
146      parser.ParseList()
147
148  def test_ParseScope(self):
149    parser = gn_helpers.GNValueParser('{a = 1}')
150    self.assertEqual(parser.ParseScope(), {'a': 1})
151
152    with self.assertRaises(gn_helpers.GNError):
153      parser = gn_helpers.GNValueParser('')  # Empty.
154      parser.ParseScope()
155    with self.assertRaises(gn_helpers.GNError):
156      parser = gn_helpers.GNValueParser('asdf')  # No {}.
157      parser.ParseScope()
158    with self.assertRaises(gn_helpers.GNError):
159      parser = gn_helpers.GNValueParser('{a = 1')  # Unterminated.
160      parser.ParseScope()
161    with self.assertRaises(gn_helpers.GNError):
162      parser = gn_helpers.GNValueParser('{"a" = 1}')  # Not identifier.
163      parser.ParseScope()
164    with self.assertRaises(gn_helpers.GNError):
165      parser = gn_helpers.GNValueParser('{a = }')  # No value.
166      parser.ParseScope()
167
168  def test_FromGNArgs(self):
169    # Booleans and numbers should work; whitespace is allowed works.
170    self.assertEqual(gn_helpers.FromGNArgs('foo = true\nbar = 1\n'),
171                     {'foo': True, 'bar': 1})
172
173    # Whitespace is not required; strings should also work.
174    self.assertEqual(gn_helpers.FromGNArgs('foo="bar baz"'),
175                     {'foo': 'bar baz'})
176
177    # Comments should work (and be ignored).
178    gn_args_lines = [
179        '# Top-level comment.',
180        'foo = true',
181        'bar = 1  # In-line comment followed by whitespace.',
182        ' ',
183        'baz = false',
184    ]
185    self.assertEqual(gn_helpers.FromGNArgs('\n'.join(gn_args_lines)), {
186        'foo': True,
187        'bar': 1,
188        'baz': False
189    })
190
191    # Lists should work.
192    self.assertEqual(gn_helpers.FromGNArgs('foo=[1, 2, 3]'),
193                     {'foo': [1, 2, 3]})
194
195    # Empty strings should return an empty dict.
196    self.assertEqual(gn_helpers.FromGNArgs(''), {})
197    self.assertEqual(gn_helpers.FromGNArgs(' \n '), {})
198
199    # Comments should work everywhere (and be ignored).
200    gn_args_lines = [
201        '# Top-level comment.',
202        '',
203        '# Variable comment.',
204        'foo = true',
205        'bar = [',
206        '    # Value comment in list.',
207        '    1,',
208        '    2,',
209        ']',
210        '',
211        'baz # Comment anywhere, really',
212        '  = # also here',
213        '    4',
214    ]
215    self.assertEqual(gn_helpers.FromGNArgs('\n'.join(gn_args_lines)), {
216        'foo': True,
217        'bar': [1, 2],
218        'baz': 4
219    })
220
221    # Scope should be parsed, even empty ones.
222    gn_args_lines = [
223        'foo = {',
224        '  a = 1',
225        '  b = [',
226        '    { },',
227        '    {',
228        '      c = 1',
229        '    },',
230        '  ]',
231        '}',
232    ]
233    self.assertEqual(gn_helpers.FromGNArgs('\n'.join(gn_args_lines)),
234                     {'foo': {
235                         'a': 1,
236                         'b': [
237                             {},
238                             {
239                                 'c': 1,
240                             },
241                         ]
242                     }})
243
244    # Non-identifiers should raise an exception.
245    with self.assertRaises(gn_helpers.GNError):
246      gn_helpers.FromGNArgs('123 = true')
247
248    # References to other variables should raise an exception.
249    with self.assertRaises(gn_helpers.GNError):
250      gn_helpers.FromGNArgs('foo = bar')
251
252    # References to functions should raise an exception.
253    with self.assertRaises(gn_helpers.GNError):
254      gn_helpers.FromGNArgs('foo = exec_script("//build/baz.py")')
255
256    # Underscores in identifiers should work.
257    self.assertEqual(gn_helpers.FromGNArgs('_foo = true'),
258                     {'_foo': True})
259    self.assertEqual(gn_helpers.FromGNArgs('foo_bar = true'),
260                     {'foo_bar': True})
261    self.assertEqual(gn_helpers.FromGNArgs('foo_=true'),
262                     {'foo_': True})
263
264  def test_ReplaceImports(self):
265    # Should be a no-op on args inputs without any imports.
266    parser = gn_helpers.GNValueParser(
267        textwrap.dedent("""
268        some_arg1 = "val1"
269        some_arg2 = "val2"
270    """))
271    parser.ReplaceImports()
272    self.assertEqual(
273        parser.input,
274        textwrap.dedent("""
275        some_arg1 = "val1"
276        some_arg2 = "val2"
277    """))
278
279    # A single "import(...)" line should be replaced with the contents of the
280    # file being imported.
281    parser = gn_helpers.GNValueParser(
282        textwrap.dedent("""
283        some_arg1 = "val1"
284        import("//some/args/file.gni")
285        some_arg2 = "val2"
286    """))
287    fake_import = 'some_imported_arg = "imported_val"'
288    builtin_var = '__builtin__' if sys.version_info.major < 3 else 'builtins'
289    open_fun = '{}.open'.format(builtin_var)
290    with mock.patch(open_fun, mock.mock_open(read_data=fake_import)):
291      parser.ReplaceImports()
292    self.assertEqual(
293        parser.input,
294        textwrap.dedent("""
295        some_arg1 = "val1"
296        some_imported_arg = "imported_val"
297        some_arg2 = "val2"
298    """))
299
300    # No trailing parenthesis should raise an exception.
301    with self.assertRaises(gn_helpers.GNError):
302      parser = gn_helpers.GNValueParser(
303          textwrap.dedent('import("//some/args/file.gni"'))
304      parser.ReplaceImports()
305
306    # No double quotes should raise an exception.
307    with self.assertRaises(gn_helpers.GNError):
308      parser = gn_helpers.GNValueParser(
309          textwrap.dedent('import(//some/args/file.gni)'))
310      parser.ReplaceImports()
311
312    # A path that's not source absolute should raise an exception.
313    with self.assertRaises(gn_helpers.GNError):
314      parser = gn_helpers.GNValueParser(
315          textwrap.dedent('import("some/relative/args/file.gni")'))
316      parser.ReplaceImports()
317
318  def test_CreateBuildCommand(self):
319    with tempfile.TemporaryDirectory() as temp_dir:
320      suffix = '.bat' if sys.platform.startswith('win32') else ''
321      self.assertEqual(f'autoninja{suffix}',
322                       gn_helpers.CreateBuildCommand(temp_dir)[0])
323
324      siso_deps = pathlib.Path(temp_dir) / '.siso_deps'
325      siso_deps.touch()
326      self.assertEqual(f'autoninja{suffix}',
327                       gn_helpers.CreateBuildCommand(temp_dir)[0])
328
329      with mock.patch('shutil.which', lambda x: None):
330        cmd = gn_helpers.CreateBuildCommand(temp_dir)
331        self.assertIn('third_party', cmd[0])
332        self.assertIn(f'{os.sep}siso', cmd[0])
333        self.assertEqual(['ninja', '-C', temp_dir], cmd[1:])
334
335      ninja_deps = pathlib.Path(temp_dir) / '.ninja_deps'
336      ninja_deps.touch()
337
338      with self.assertRaisesRegex(Exception, 'Found both'):
339        gn_helpers.CreateBuildCommand(temp_dir)
340
341      siso_deps.unlink()
342      self.assertEqual(f'autoninja{suffix}',
343                       gn_helpers.CreateBuildCommand(temp_dir)[0])
344
345      with mock.patch('shutil.which', lambda x: None):
346        cmd = gn_helpers.CreateBuildCommand(temp_dir)
347        self.assertIn('third_party', cmd[0])
348        self.assertIn(f'{os.sep}ninja', cmd[0])
349        self.assertEqual(['-C', temp_dir], cmd[1:])
350
351
352if __name__ == '__main__':
353  unittest.main()
354