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