1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function, unicode_literals 6 7import unittest 8 9from mozunit import main 10 11from mozbuild.frontend.reader import ( 12 MozbuildSandbox, 13 SandboxCalledError, 14) 15 16from mozbuild.frontend.sandbox import ( 17 Sandbox, 18 SandboxExecutionError, 19 SandboxLoadError, 20) 21 22from mozbuild.frontend.context import ( 23 Context, 24 FUNCTIONS, 25 SourcePath, 26 SPECIAL_VARIABLES, 27 VARIABLES, 28) 29 30from mozbuild.test.common import MockConfig 31 32import mozpack.path as mozpath 33 34test_data_path = mozpath.abspath(mozpath.dirname(__file__)) 35test_data_path = mozpath.join(test_data_path, "data") 36 37 38class TestSandbox(unittest.TestCase): 39 def sandbox(self): 40 return Sandbox( 41 Context( 42 { 43 "DIRS": (list, list, None), 44 } 45 ) 46 ) 47 48 def test_exec_source_success(self): 49 sandbox = self.sandbox() 50 context = sandbox._context 51 52 sandbox.exec_source("foo = True", mozpath.abspath("foo.py")) 53 54 self.assertNotIn("foo", context) 55 self.assertEqual(context.main_path, mozpath.abspath("foo.py")) 56 self.assertEqual(context.all_paths, set([mozpath.abspath("foo.py")])) 57 58 def test_exec_compile_error(self): 59 sandbox = self.sandbox() 60 61 with self.assertRaises(SandboxExecutionError) as se: 62 sandbox.exec_source("2f23;k;asfj", mozpath.abspath("foo.py")) 63 64 self.assertEqual(se.exception.file_stack, [mozpath.abspath("foo.py")]) 65 self.assertIsInstance(se.exception.exc_value, SyntaxError) 66 self.assertEqual(sandbox._context.main_path, mozpath.abspath("foo.py")) 67 68 def test_exec_import_denied(self): 69 sandbox = self.sandbox() 70 71 with self.assertRaises(SandboxExecutionError) as se: 72 sandbox.exec_source("import sys") 73 74 self.assertIsInstance(se.exception, SandboxExecutionError) 75 self.assertEqual(se.exception.exc_type, ImportError) 76 77 def test_exec_source_multiple(self): 78 sandbox = self.sandbox() 79 80 sandbox.exec_source('DIRS = ["foo"]') 81 sandbox.exec_source('DIRS += ["bar"]') 82 83 self.assertEqual(sandbox["DIRS"], ["foo", "bar"]) 84 85 def test_exec_source_illegal_key_set(self): 86 sandbox = self.sandbox() 87 88 with self.assertRaises(SandboxExecutionError) as se: 89 sandbox.exec_source("ILLEGAL = True") 90 91 e = se.exception 92 self.assertIsInstance(e.exc_value, KeyError) 93 94 e = se.exception.exc_value 95 self.assertEqual(e.args[0], "global_ns") 96 self.assertEqual(e.args[1], "set_unknown") 97 98 def test_exec_source_reassign(self): 99 sandbox = self.sandbox() 100 101 sandbox.exec_source('DIRS = ["foo"]') 102 with self.assertRaises(SandboxExecutionError) as se: 103 sandbox.exec_source('DIRS = ["bar"]') 104 105 self.assertEqual(sandbox["DIRS"], ["foo"]) 106 e = se.exception 107 self.assertIsInstance(e.exc_value, KeyError) 108 109 e = se.exception.exc_value 110 self.assertEqual(e.args[0], "global_ns") 111 self.assertEqual(e.args[1], "reassign") 112 self.assertEqual(e.args[2], "DIRS") 113 114 def test_exec_source_reassign_builtin(self): 115 sandbox = self.sandbox() 116 117 with self.assertRaises(SandboxExecutionError) as se: 118 sandbox.exec_source("sorted = 1") 119 120 e = se.exception 121 self.assertIsInstance(e.exc_value, KeyError) 122 123 e = se.exception.exc_value 124 self.assertEqual(e.args[0], "Cannot reassign builtins") 125 126 127class TestedSandbox(MozbuildSandbox): 128 """Version of MozbuildSandbox with a little more convenience for testing. 129 130 It automatically normalizes paths given to exec_file and exec_source. This 131 helps simplify the test code. 132 """ 133 134 def normalize_path(self, path): 135 return mozpath.normpath(mozpath.join(self._context.config.topsrcdir, path)) 136 137 def source_path(self, path): 138 return SourcePath(self._context, path) 139 140 def exec_file(self, path): 141 super(TestedSandbox, self).exec_file(self.normalize_path(path)) 142 143 def exec_source(self, source, path=""): 144 super(TestedSandbox, self).exec_source( 145 source, self.normalize_path(path) if path else "" 146 ) 147 148 149class TestMozbuildSandbox(unittest.TestCase): 150 def sandbox(self, data_path=None, metadata={}): 151 config = None 152 153 if data_path is not None: 154 config = MockConfig(mozpath.join(test_data_path, data_path)) 155 else: 156 config = MockConfig() 157 158 return TestedSandbox(Context(VARIABLES, config), metadata) 159 160 def test_default_state(self): 161 sandbox = self.sandbox() 162 sandbox._context.add_source(sandbox.normalize_path("moz.build")) 163 config = sandbox._context.config 164 165 self.assertEqual(sandbox["TOPSRCDIR"], config.topsrcdir) 166 self.assertEqual(sandbox["TOPOBJDIR"], config.topobjdir) 167 self.assertEqual(sandbox["RELATIVEDIR"], "") 168 self.assertEqual(sandbox["SRCDIR"], config.topsrcdir) 169 self.assertEqual(sandbox["OBJDIR"], config.topobjdir) 170 171 def test_symbol_presence(self): 172 # Ensure no discrepancies between the master symbol table and what's in 173 # the sandbox. 174 sandbox = self.sandbox() 175 sandbox._context.add_source(sandbox.normalize_path("moz.build")) 176 177 all_symbols = set() 178 all_symbols |= set(FUNCTIONS.keys()) 179 all_symbols |= set(SPECIAL_VARIABLES.keys()) 180 181 for symbol in all_symbols: 182 self.assertIsNotNone(sandbox[symbol]) 183 184 def test_path_calculation(self): 185 sandbox = self.sandbox() 186 sandbox._context.add_source(sandbox.normalize_path("foo/bar/moz.build")) 187 config = sandbox._context.config 188 189 self.assertEqual(sandbox["TOPSRCDIR"], config.topsrcdir) 190 self.assertEqual(sandbox["TOPOBJDIR"], config.topobjdir) 191 self.assertEqual(sandbox["RELATIVEDIR"], "foo/bar") 192 self.assertEqual(sandbox["SRCDIR"], mozpath.join(config.topsrcdir, "foo/bar")) 193 self.assertEqual(sandbox["OBJDIR"], mozpath.join(config.topobjdir, "foo/bar")) 194 195 def test_config_access(self): 196 sandbox = self.sandbox() 197 config = sandbox._context.config 198 199 self.assertEqual(sandbox["CONFIG"]["MOZ_TRUE"], "1") 200 self.assertEqual(sandbox["CONFIG"]["MOZ_FOO"], config.substs["MOZ_FOO"]) 201 202 # Access to an undefined substitution should return None. 203 self.assertNotIn("MISSING", sandbox["CONFIG"]) 204 self.assertIsNone(sandbox["CONFIG"]["MISSING"]) 205 206 # Should shouldn't be allowed to assign to the config. 207 with self.assertRaises(Exception): 208 sandbox["CONFIG"]["FOO"] = "" 209 210 def test_special_variables(self): 211 sandbox = self.sandbox() 212 sandbox._context.add_source(sandbox.normalize_path("moz.build")) 213 214 for k in SPECIAL_VARIABLES: 215 with self.assertRaises(KeyError): 216 sandbox[k] = 0 217 218 def test_exec_source_reassign_exported(self): 219 template_sandbox = self.sandbox(data_path="templates") 220 221 # Templates need to be defined in actual files because of 222 # inspect.getsourcelines. 223 template_sandbox.exec_file("templates.mozbuild") 224 225 config = MockConfig() 226 227 exports = {"DIST_SUBDIR": "browser"} 228 229 sandbox = TestedSandbox( 230 Context(VARIABLES, config), 231 metadata={ 232 "exports": exports, 233 "templates": template_sandbox.templates, 234 }, 235 ) 236 237 self.assertEqual(sandbox["DIST_SUBDIR"], "browser") 238 239 # Templates should not interfere 240 sandbox.exec_source("Template([])", "foo.mozbuild") 241 242 sandbox.exec_source('DIST_SUBDIR = "foo"') 243 with self.assertRaises(SandboxExecutionError) as se: 244 sandbox.exec_source('DIST_SUBDIR = "bar"') 245 246 self.assertEqual(sandbox["DIST_SUBDIR"], "foo") 247 e = se.exception 248 self.assertIsInstance(e.exc_value, KeyError) 249 250 e = se.exception.exc_value 251 self.assertEqual(e.args[0], "global_ns") 252 self.assertEqual(e.args[1], "reassign") 253 self.assertEqual(e.args[2], "DIST_SUBDIR") 254 255 def test_include_basic(self): 256 sandbox = self.sandbox(data_path="include-basic") 257 258 sandbox.exec_file("moz.build") 259 260 self.assertEqual( 261 sandbox["DIRS"], 262 [ 263 sandbox.source_path("foo"), 264 sandbox.source_path("bar"), 265 ], 266 ) 267 self.assertEqual( 268 sandbox._context.main_path, sandbox.normalize_path("moz.build") 269 ) 270 self.assertEqual(len(sandbox._context.all_paths), 2) 271 272 def test_include_outside_topsrcdir(self): 273 sandbox = self.sandbox(data_path="include-outside-topsrcdir") 274 275 with self.assertRaises(SandboxLoadError) as se: 276 sandbox.exec_file("relative.build") 277 278 self.assertEqual( 279 se.exception.illegal_path, sandbox.normalize_path("../moz.build") 280 ) 281 282 def test_include_error_stack(self): 283 # Ensure the path stack is reported properly in exceptions. 284 sandbox = self.sandbox(data_path="include-file-stack") 285 286 with self.assertRaises(SandboxExecutionError) as se: 287 sandbox.exec_file("moz.build") 288 289 e = se.exception 290 self.assertIsInstance(e.exc_value, KeyError) 291 292 args = e.exc_value.args 293 self.assertEqual(args[0], "global_ns") 294 self.assertEqual(args[1], "set_unknown") 295 self.assertEqual(args[2], "ILLEGAL") 296 297 expected_stack = [ 298 mozpath.join(sandbox._context.config.topsrcdir, p) 299 for p in ["moz.build", "included-1.build", "included-2.build"] 300 ] 301 302 self.assertEqual(e.file_stack, expected_stack) 303 304 def test_include_missing(self): 305 sandbox = self.sandbox(data_path="include-missing") 306 307 with self.assertRaises(SandboxLoadError) as sle: 308 sandbox.exec_file("moz.build") 309 310 self.assertIsNotNone(sle.exception.read_error) 311 312 def test_include_relative_from_child_dir(self): 313 # A relative path from a subdirectory should be relative from that 314 # child directory. 315 sandbox = self.sandbox(data_path="include-relative-from-child") 316 sandbox.exec_file("child/child.build") 317 self.assertEqual(sandbox["DIRS"], [sandbox.source_path("../foo")]) 318 319 sandbox = self.sandbox(data_path="include-relative-from-child") 320 sandbox.exec_file("child/child2.build") 321 self.assertEqual(sandbox["DIRS"], [sandbox.source_path("../foo")]) 322 323 def test_include_topsrcdir_relative(self): 324 # An absolute path for include() is relative to topsrcdir. 325 326 sandbox = self.sandbox(data_path="include-topsrcdir-relative") 327 sandbox.exec_file("moz.build") 328 329 self.assertEqual(sandbox["DIRS"], [sandbox.source_path("foo")]) 330 331 def test_error(self): 332 sandbox = self.sandbox() 333 334 with self.assertRaises(SandboxCalledError) as sce: 335 sandbox.exec_source('error("This is an error.")') 336 337 e = sce.exception.message 338 self.assertIn("This is an error.", str(e)) 339 340 def test_substitute_config_files(self): 341 sandbox = self.sandbox() 342 sandbox._context.add_source(sandbox.normalize_path("moz.build")) 343 344 sandbox.exec_source('CONFIGURE_SUBST_FILES += ["bar", "foo"]') 345 self.assertEqual(sandbox["CONFIGURE_SUBST_FILES"], ["bar", "foo"]) 346 for item in sandbox["CONFIGURE_SUBST_FILES"]: 347 self.assertIsInstance(item, SourcePath) 348 349 def test_invalid_exports_set_base(self): 350 sandbox = self.sandbox() 351 352 with self.assertRaises(SandboxExecutionError) as se: 353 sandbox.exec_source('EXPORTS = "foo.h"') 354 355 self.assertEqual(se.exception.exc_type, ValueError) 356 357 def test_templates(self): 358 sandbox = self.sandbox(data_path="templates") 359 360 # Templates need to be defined in actual files because of 361 # inspect.getsourcelines. 362 sandbox.exec_file("templates.mozbuild") 363 364 sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) 365 source = """ 366Template([ 367 'foo.cpp', 368]) 369""" 370 sandbox2.exec_source(source, "foo.mozbuild") 371 372 self.assertEqual( 373 sandbox2._context, 374 { 375 "SOURCES": ["foo.cpp"], 376 "DIRS": [], 377 }, 378 ) 379 380 sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) 381 source = """ 382SOURCES += ['qux.cpp'] 383Template([ 384 'bar.cpp', 385 'foo.cpp', 386],[ 387 'foo', 388]) 389SOURCES += ['hoge.cpp'] 390""" 391 sandbox2.exec_source(source, "foo.mozbuild") 392 393 self.assertEqual( 394 sandbox2._context, 395 { 396 "SOURCES": ["qux.cpp", "bar.cpp", "foo.cpp", "hoge.cpp"], 397 "DIRS": [sandbox2.source_path("foo")], 398 }, 399 ) 400 401 sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) 402 source = """ 403TemplateError([ 404 'foo.cpp', 405]) 406""" 407 with self.assertRaises(SandboxExecutionError) as se: 408 sandbox2.exec_source(source, "foo.mozbuild") 409 410 e = se.exception 411 self.assertIsInstance(e.exc_value, KeyError) 412 413 e = se.exception.exc_value 414 self.assertEqual(e.args[0], "global_ns") 415 self.assertEqual(e.args[1], "set_unknown") 416 417 # TemplateGlobalVariable tries to access 'illegal' but that is expected 418 # to throw. 419 sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) 420 source = """ 421illegal = True 422TemplateGlobalVariable() 423""" 424 with self.assertRaises(SandboxExecutionError) as se: 425 sandbox2.exec_source(source, "foo.mozbuild") 426 427 e = se.exception 428 self.assertIsInstance(e.exc_value, NameError) 429 430 # TemplateGlobalUPPERVariable sets SOURCES with DIRS, but the context 431 # used when running the template is not expected to access variables 432 # from the global context. 433 sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) 434 source = """ 435DIRS += ['foo'] 436TemplateGlobalUPPERVariable() 437""" 438 sandbox2.exec_source(source, "foo.mozbuild") 439 self.assertEqual( 440 sandbox2._context, 441 { 442 "SOURCES": [], 443 "DIRS": [sandbox2.source_path("foo")], 444 }, 445 ) 446 447 # However, the result of the template is mixed with the global 448 # context. 449 sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) 450 source = """ 451SOURCES += ['qux.cpp'] 452TemplateInherit([ 453 'bar.cpp', 454 'foo.cpp', 455]) 456SOURCES += ['hoge.cpp'] 457""" 458 sandbox2.exec_source(source, "foo.mozbuild") 459 460 self.assertEqual( 461 sandbox2._context, 462 { 463 "SOURCES": ["qux.cpp", "bar.cpp", "foo.cpp", "hoge.cpp"], 464 "USE_LIBS": ["foo"], 465 "DIRS": [], 466 }, 467 ) 468 469 # Template names must be CamelCase. Here, we can define the template 470 # inline because the error happens before inspect.getsourcelines. 471 sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) 472 source = """ 473@template 474def foo(): 475 pass 476""" 477 478 with self.assertRaises(SandboxExecutionError) as se: 479 sandbox2.exec_source(source, "foo.mozbuild") 480 481 e = se.exception 482 self.assertIsInstance(e.exc_value, NameError) 483 484 e = se.exception.exc_value 485 self.assertIn("Template function names must be CamelCase.", str(e)) 486 487 # Template names must not already be registered. 488 sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) 489 source = """ 490@template 491def Template(): 492 pass 493""" 494 with self.assertRaises(SandboxExecutionError) as se: 495 sandbox2.exec_source(source, "foo.mozbuild") 496 497 e = se.exception 498 self.assertIsInstance(e.exc_value, KeyError) 499 500 e = se.exception.exc_value 501 self.assertIn( 502 'A template named "Template" was already declared in %s.' 503 % sandbox.normalize_path("templates.mozbuild"), 504 str(e), 505 ) 506 507 def test_function_args(self): 508 class Foo(int): 509 pass 510 511 def foo(a, b): 512 return type(a), type(b) 513 514 FUNCTIONS.update( 515 { 516 "foo": (lambda self: foo, (Foo, int), ""), 517 } 518 ) 519 520 try: 521 sandbox = self.sandbox() 522 source = 'foo("a", "b")' 523 524 with self.assertRaises(SandboxExecutionError) as se: 525 sandbox.exec_source(source, "foo.mozbuild") 526 527 e = se.exception 528 self.assertIsInstance(e.exc_value, ValueError) 529 530 sandbox = self.sandbox() 531 source = 'foo(1, "b")' 532 533 with self.assertRaises(SandboxExecutionError) as se: 534 sandbox.exec_source(source, "foo.mozbuild") 535 536 e = se.exception 537 self.assertIsInstance(e.exc_value, ValueError) 538 539 sandbox = self.sandbox() 540 source = "a = foo(1, 2)" 541 sandbox.exec_source(source, "foo.mozbuild") 542 543 self.assertEquals(sandbox["a"], (Foo, int)) 544 finally: 545 del FUNCTIONS["foo"] 546 547 548if __name__ == "__main__": 549 main() 550