blob: 34d651c8c4b009e8da759fb4c37cd5f740c3893c [file] [log] [blame]
# 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/.
from __future__ import unicode_literals
import os
import shutil
import unittest
from mozunit import main
from mozbuild.frontend.reader import (
MozbuildSandbox,
SandboxCalledError,
)
from mozbuild.frontend.sandbox import (
Sandbox,
SandboxExecutionError,
SandboxLoadError,
)
from mozbuild.frontend.context import (
Context,
FUNCTIONS,
SourcePath,
SPECIAL_VARIABLES,
VARIABLES,
)
from mozbuild.test.common import MockConfig
from types import StringTypes
import mozpack.path as mozpath
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, 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('True = 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
self.assertEqual(e.message, 'This is an error.')
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_utf8_substs(self):
"""Ensure invalid UTF-8 in substs is converted with an error."""
# This is really mbcs. It's a bunch of invalid UTF-8.
config = MockConfig(extra_substs={'BAD_UTF8': b'\x83\x81\x83\x82\x3A'})
sandbox = MozbuildSandbox(Context(VARIABLES, config))
self.assertEqual(sandbox['CONFIG']['BAD_UTF8'],
u'\ufffd\ufffd\ufffd\ufffd:')
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.assertEqual(e.message,
'Template function names must be CamelCase.')
# 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.assertEqual(e.message,
'A template named "Template" was already declared in %s.' %
sandbox.normalize_path('templates.mozbuild'))
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.assertEquals(sandbox['a'], (Foo, int))
finally:
del FUNCTIONS['foo']
if __name__ == '__main__':
main()