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