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