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