summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/test/frontend/test_sandbox.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozbuild/mozbuild/test/frontend/test_sandbox.py')
-rw-r--r--python/mozbuild/mozbuild/test/frontend/test_sandbox.py536
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()