diff options
Diffstat (limited to 'python/mozbuild/mozbuild/test/frontend/test_sandbox.py')
-rw-r--r-- | python/mozbuild/mozbuild/test/frontend/test_sandbox.py | 536 |
1 files changed, 536 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/test/frontend/test_sandbox.py b/python/mozbuild/mozbuild/test/frontend/test_sandbox.py new file mode 100644 index 0000000000..017de1ce9c --- /dev/null +++ b/python/mozbuild/mozbuild/test/frontend/test_sandbox.py @@ -0,0 +1,536 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import unittest + +import mozpack.path as mozpath +from mozunit import main + +from mozbuild.frontend.context import ( + FUNCTIONS, + SPECIAL_VARIABLES, + VARIABLES, + Context, + SourcePath, +) +from mozbuild.frontend.reader import MozbuildSandbox, SandboxCalledError +from mozbuild.frontend.sandbox import Sandbox, SandboxExecutionError, SandboxLoadError +from mozbuild.test.common import MockConfig + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +class TestSandbox(unittest.TestCase): + def sandbox(self): + return Sandbox( + Context( + { + "DIRS": (list, list, None), + } + ) + ) + + def test_exec_source_success(self): + sandbox = self.sandbox() + context = sandbox._context + + sandbox.exec_source("foo = True", mozpath.abspath("foo.py")) + + self.assertNotIn("foo", context) + self.assertEqual(context.main_path, mozpath.abspath("foo.py")) + self.assertEqual(context.all_paths, set([mozpath.abspath("foo.py")])) + + def test_exec_compile_error(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source("2f23;k;asfj", mozpath.abspath("foo.py")) + + self.assertEqual(se.exception.file_stack, [mozpath.abspath("foo.py")]) + self.assertIsInstance(se.exception.exc_value, SyntaxError) + self.assertEqual(sandbox._context.main_path, mozpath.abspath("foo.py")) + + def test_exec_import_denied(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source("import sys") + + self.assertIsInstance(se.exception, SandboxExecutionError) + self.assertEqual(se.exception.exc_type, ImportError) + + def test_exec_source_multiple(self): + sandbox = self.sandbox() + + sandbox.exec_source('DIRS = ["foo"]') + sandbox.exec_source('DIRS += ["bar"]') + + self.assertEqual(sandbox["DIRS"], ["foo", "bar"]) + + def test_exec_source_illegal_key_set(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source("ILLEGAL = True") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "global_ns") + self.assertEqual(e.args[1], "set_unknown") + + def test_exec_source_reassign(self): + sandbox = self.sandbox() + + sandbox.exec_source('DIRS = ["foo"]') + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source('DIRS = ["bar"]') + + self.assertEqual(sandbox["DIRS"], ["foo"]) + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "global_ns") + self.assertEqual(e.args[1], "reassign") + self.assertEqual(e.args[2], "DIRS") + + def test_exec_source_reassign_builtin(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source("sorted = 1") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "Cannot reassign builtins") + + +class TestedSandbox(MozbuildSandbox): + """Version of MozbuildSandbox with a little more convenience for testing. + + It automatically normalizes paths given to exec_file and exec_source. This + helps simplify the test code. + """ + + def normalize_path(self, path): + return mozpath.normpath(mozpath.join(self._context.config.topsrcdir, path)) + + def source_path(self, path): + return SourcePath(self._context, path) + + def exec_file(self, path): + super(TestedSandbox, self).exec_file(self.normalize_path(path)) + + def exec_source(self, source, path=""): + super(TestedSandbox, self).exec_source( + source, self.normalize_path(path) if path else "" + ) + + +class TestMozbuildSandbox(unittest.TestCase): + def sandbox(self, data_path=None, metadata={}): + config = None + + if data_path is not None: + config = MockConfig(mozpath.join(test_data_path, data_path)) + else: + config = MockConfig() + + return TestedSandbox(Context(VARIABLES, config), metadata) + + def test_default_state(self): + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("moz.build")) + config = sandbox._context.config + + self.assertEqual(sandbox["TOPSRCDIR"], config.topsrcdir) + self.assertEqual(sandbox["TOPOBJDIR"], config.topobjdir) + self.assertEqual(sandbox["RELATIVEDIR"], "") + self.assertEqual(sandbox["SRCDIR"], config.topsrcdir) + self.assertEqual(sandbox["OBJDIR"], config.topobjdir) + + def test_symbol_presence(self): + # Ensure no discrepancies between the master symbol table and what's in + # the sandbox. + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("moz.build")) + + all_symbols = set() + all_symbols |= set(FUNCTIONS.keys()) + all_symbols |= set(SPECIAL_VARIABLES.keys()) + + for symbol in all_symbols: + self.assertIsNotNone(sandbox[symbol]) + + def test_path_calculation(self): + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("foo/bar/moz.build")) + config = sandbox._context.config + + self.assertEqual(sandbox["TOPSRCDIR"], config.topsrcdir) + self.assertEqual(sandbox["TOPOBJDIR"], config.topobjdir) + self.assertEqual(sandbox["RELATIVEDIR"], "foo/bar") + self.assertEqual(sandbox["SRCDIR"], mozpath.join(config.topsrcdir, "foo/bar")) + self.assertEqual(sandbox["OBJDIR"], mozpath.join(config.topobjdir, "foo/bar")) + + def test_config_access(self): + sandbox = self.sandbox() + config = sandbox._context.config + + self.assertEqual(sandbox["CONFIG"]["MOZ_TRUE"], "1") + self.assertEqual(sandbox["CONFIG"]["MOZ_FOO"], config.substs["MOZ_FOO"]) + + # Access to an undefined substitution should return None. + self.assertNotIn("MISSING", sandbox["CONFIG"]) + self.assertIsNone(sandbox["CONFIG"]["MISSING"]) + + # Should shouldn't be allowed to assign to the config. + with self.assertRaises(Exception): + sandbox["CONFIG"]["FOO"] = "" + + def test_special_variables(self): + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("moz.build")) + + for k in SPECIAL_VARIABLES: + with self.assertRaises(KeyError): + sandbox[k] = 0 + + def test_exec_source_reassign_exported(self): + template_sandbox = self.sandbox(data_path="templates") + + # Templates need to be defined in actual files because of + # inspect.getsourcelines. + template_sandbox.exec_file("templates.mozbuild") + + config = MockConfig() + + exports = {"DIST_SUBDIR": "browser"} + + sandbox = TestedSandbox( + Context(VARIABLES, config), + metadata={ + "exports": exports, + "templates": template_sandbox.templates, + }, + ) + + self.assertEqual(sandbox["DIST_SUBDIR"], "browser") + + # Templates should not interfere + sandbox.exec_source("Template([])", "foo.mozbuild") + + sandbox.exec_source('DIST_SUBDIR = "foo"') + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source('DIST_SUBDIR = "bar"') + + self.assertEqual(sandbox["DIST_SUBDIR"], "foo") + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "global_ns") + self.assertEqual(e.args[1], "reassign") + self.assertEqual(e.args[2], "DIST_SUBDIR") + + def test_include_basic(self): + sandbox = self.sandbox(data_path="include-basic") + + sandbox.exec_file("moz.build") + + self.assertEqual( + sandbox["DIRS"], + [ + sandbox.source_path("foo"), + sandbox.source_path("bar"), + ], + ) + self.assertEqual( + sandbox._context.main_path, sandbox.normalize_path("moz.build") + ) + self.assertEqual(len(sandbox._context.all_paths), 2) + + def test_include_outside_topsrcdir(self): + sandbox = self.sandbox(data_path="include-outside-topsrcdir") + + with self.assertRaises(SandboxLoadError) as se: + sandbox.exec_file("relative.build") + + self.assertEqual( + se.exception.illegal_path, sandbox.normalize_path("../moz.build") + ) + + def test_include_error_stack(self): + # Ensure the path stack is reported properly in exceptions. + sandbox = self.sandbox(data_path="include-file-stack") + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_file("moz.build") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + args = e.exc_value.args + self.assertEqual(args[0], "global_ns") + self.assertEqual(args[1], "set_unknown") + self.assertEqual(args[2], "ILLEGAL") + + expected_stack = [ + mozpath.join(sandbox._context.config.topsrcdir, p) + for p in ["moz.build", "included-1.build", "included-2.build"] + ] + + self.assertEqual(e.file_stack, expected_stack) + + def test_include_missing(self): + sandbox = self.sandbox(data_path="include-missing") + + with self.assertRaises(SandboxLoadError) as sle: + sandbox.exec_file("moz.build") + + self.assertIsNotNone(sle.exception.read_error) + + def test_include_relative_from_child_dir(self): + # A relative path from a subdirectory should be relative from that + # child directory. + sandbox = self.sandbox(data_path="include-relative-from-child") + sandbox.exec_file("child/child.build") + self.assertEqual(sandbox["DIRS"], [sandbox.source_path("../foo")]) + + sandbox = self.sandbox(data_path="include-relative-from-child") + sandbox.exec_file("child/child2.build") + self.assertEqual(sandbox["DIRS"], [sandbox.source_path("../foo")]) + + def test_include_topsrcdir_relative(self): + # An absolute path for include() is relative to topsrcdir. + + sandbox = self.sandbox(data_path="include-topsrcdir-relative") + sandbox.exec_file("moz.build") + + self.assertEqual(sandbox["DIRS"], [sandbox.source_path("foo")]) + + def test_error(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxCalledError) as sce: + sandbox.exec_source('error("This is an error.")') + + e = sce.exception.message + self.assertIn("This is an error.", str(e)) + + def test_substitute_config_files(self): + sandbox = self.sandbox() + sandbox._context.add_source(sandbox.normalize_path("moz.build")) + + sandbox.exec_source('CONFIGURE_SUBST_FILES += ["bar", "foo"]') + self.assertEqual(sandbox["CONFIGURE_SUBST_FILES"], ["bar", "foo"]) + for item in sandbox["CONFIGURE_SUBST_FILES"]: + self.assertIsInstance(item, SourcePath) + + def test_invalid_exports_set_base(self): + sandbox = self.sandbox() + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source('EXPORTS = "foo.h"') + + self.assertEqual(se.exception.exc_type, ValueError) + + def test_templates(self): + sandbox = self.sandbox(data_path="templates") + + # Templates need to be defined in actual files because of + # inspect.getsourcelines. + sandbox.exec_file("templates.mozbuild") + + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +Template([ + 'foo.cpp', +]) +""" + sandbox2.exec_source(source, "foo.mozbuild") + + self.assertEqual( + sandbox2._context, + { + "SOURCES": ["foo.cpp"], + "DIRS": [], + }, + ) + + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +SOURCES += ['qux.cpp'] +Template([ + 'bar.cpp', + 'foo.cpp', +],[ + 'foo', +]) +SOURCES += ['hoge.cpp'] +""" + sandbox2.exec_source(source, "foo.mozbuild") + + self.assertEqual( + sandbox2._context, + { + "SOURCES": ["qux.cpp", "bar.cpp", "foo.cpp", "hoge.cpp"], + "DIRS": [sandbox2.source_path("foo")], + }, + ) + + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +TemplateError([ + 'foo.cpp', +]) +""" + with self.assertRaises(SandboxExecutionError) as se: + sandbox2.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertEqual(e.args[0], "global_ns") + self.assertEqual(e.args[1], "set_unknown") + + # TemplateGlobalVariable tries to access 'illegal' but that is expected + # to throw. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +illegal = True +TemplateGlobalVariable() +""" + with self.assertRaises(SandboxExecutionError) as se: + sandbox2.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, NameError) + + # TemplateGlobalUPPERVariable sets SOURCES with DIRS, but the context + # used when running the template is not expected to access variables + # from the global context. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +DIRS += ['foo'] +TemplateGlobalUPPERVariable() +""" + sandbox2.exec_source(source, "foo.mozbuild") + self.assertEqual( + sandbox2._context, + { + "SOURCES": [], + "DIRS": [sandbox2.source_path("foo")], + }, + ) + + # However, the result of the template is mixed with the global + # context. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +SOURCES += ['qux.cpp'] +TemplateInherit([ + 'bar.cpp', + 'foo.cpp', +]) +SOURCES += ['hoge.cpp'] +""" + sandbox2.exec_source(source, "foo.mozbuild") + + self.assertEqual( + sandbox2._context, + { + "SOURCES": ["qux.cpp", "bar.cpp", "foo.cpp", "hoge.cpp"], + "USE_LIBS": ["foo"], + "DIRS": [], + }, + ) + + # Template names must be CamelCase. Here, we can define the template + # inline because the error happens before inspect.getsourcelines. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +@template +def foo(): + pass +""" + + with self.assertRaises(SandboxExecutionError) as se: + sandbox2.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, NameError) + + e = se.exception.exc_value + self.assertIn("Template function names must be CamelCase.", str(e)) + + # Template names must not already be registered. + sandbox2 = self.sandbox(metadata={"templates": sandbox.templates}) + source = """ +@template +def Template(): + pass +""" + with self.assertRaises(SandboxExecutionError) as se: + sandbox2.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, KeyError) + + e = se.exception.exc_value + self.assertIn( + 'A template named "Template" was already declared in %s.' + % sandbox.normalize_path("templates.mozbuild"), + str(e), + ) + + def test_function_args(self): + class Foo(int): + pass + + def foo(a, b): + return type(a), type(b) + + FUNCTIONS.update( + { + "foo": (lambda self: foo, (Foo, int), ""), + } + ) + + try: + sandbox = self.sandbox() + source = 'foo("a", "b")' + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, ValueError) + + sandbox = self.sandbox() + source = 'foo(1, "b")' + + with self.assertRaises(SandboxExecutionError) as se: + sandbox.exec_source(source, "foo.mozbuild") + + e = se.exception + self.assertIsInstance(e.exc_value, ValueError) + + sandbox = self.sandbox() + source = "a = foo(1, 2)" + sandbox.exec_source(source, "foo.mozbuild") + + self.assertEqual(sandbox["a"], (Foo, int)) + finally: + del FUNCTIONS["foo"] + + +if __name__ == "__main__": + main() |