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 unittest
9
10from shutil import rmtree
11
12from tempfile import (
13    gettempdir,
14    mkdtemp,
15)
16
17from mozfile.mozfile import NamedTemporaryFile
18
19from mozunit import main
20
21from mozbuild.mozconfig import (
22    MozconfigFindException,
23    MozconfigLoadException,
24    MozconfigLoader,
25)
26
27
28class TestMozconfigLoader(unittest.TestCase):
29    def setUp(self):
30        self._old_env = dict(os.environ)
31        os.environ.pop('MOZCONFIG', None)
32        os.environ.pop('MOZ_OBJDIR', None)
33        os.environ.pop('CC', None)
34        os.environ.pop('CXX', None)
35        self._temp_dirs = set()
36
37    def tearDown(self):
38        os.environ.clear()
39        os.environ.update(self._old_env)
40
41        for d in self._temp_dirs:
42            rmtree(d)
43
44    def get_loader(self):
45        return MozconfigLoader(self.get_temp_dir())
46
47    def get_temp_dir(self):
48        d = mkdtemp()
49        self._temp_dirs.add(d)
50
51        return d
52
53    def test_find_legacy_env(self):
54        """Ensure legacy mozconfig path definitions result in error."""
55
56        os.environ[b'MOZ_MYCONFIG'] = '/foo'
57
58        with self.assertRaises(MozconfigFindException) as e:
59            self.get_loader().find_mozconfig()
60
61        self.assertTrue(e.exception.message.startswith('The MOZ_MYCONFIG'))
62
63    def test_find_multiple_configs(self):
64        """Ensure multiple relative-path MOZCONFIGs result in error."""
65        relative_mozconfig = '.mconfig'
66        os.environ[b'MOZCONFIG'] = relative_mozconfig
67
68        srcdir = self.get_temp_dir()
69        curdir = self.get_temp_dir()
70        dirs = [srcdir, curdir]
71        loader = MozconfigLoader(srcdir)
72        for d in dirs:
73            path = os.path.join(d, relative_mozconfig)
74            with open(path, 'wb') as f:
75                f.write(path)
76
77        orig_dir = os.getcwd()
78        try:
79            os.chdir(curdir)
80            with self.assertRaises(MozconfigFindException) as e:
81                loader.find_mozconfig()
82        finally:
83            os.chdir(orig_dir)
84
85        self.assertIn('exists in more than one of', e.exception.message)
86        for d in dirs:
87            self.assertIn(d, e.exception.message)
88
89    def test_find_multiple_but_identical_configs(self):
90        """Ensure multiple relative-path MOZCONFIGs pointing at the same file are OK."""
91        relative_mozconfig = '../src/.mconfig'
92        os.environ[b'MOZCONFIG'] = relative_mozconfig
93
94        topdir = self.get_temp_dir()
95        srcdir = os.path.join(topdir, 'src')
96        os.mkdir(srcdir)
97        curdir = os.path.join(topdir, 'obj')
98        os.mkdir(curdir)
99
100        loader = MozconfigLoader(srcdir)
101        path = os.path.join(srcdir, relative_mozconfig)
102        with open(path, 'w'):
103            pass
104
105        orig_dir = os.getcwd()
106        try:
107            os.chdir(curdir)
108            self.assertEqual(os.path.realpath(loader.find_mozconfig()),
109                             os.path.realpath(path))
110        finally:
111            os.chdir(orig_dir)
112
113    def test_find_no_relative_configs(self):
114        """Ensure a missing relative-path MOZCONFIG is detected."""
115        relative_mozconfig = '.mconfig'
116        os.environ[b'MOZCONFIG'] = relative_mozconfig
117
118        srcdir = self.get_temp_dir()
119        curdir = self.get_temp_dir()
120        dirs = [srcdir, curdir]
121        loader = MozconfigLoader(srcdir)
122
123        orig_dir = os.getcwd()
124        try:
125            os.chdir(curdir)
126            with self.assertRaises(MozconfigFindException) as e:
127                loader.find_mozconfig()
128        finally:
129            os.chdir(orig_dir)
130
131        self.assertIn('does not exist in any of', e.exception.message)
132        for d in dirs:
133            self.assertIn(d, e.exception.message)
134
135    def test_find_relative_mozconfig(self):
136        """Ensure a relative MOZCONFIG can be found in the srcdir."""
137        relative_mozconfig = '.mconfig'
138        os.environ[b'MOZCONFIG'] = relative_mozconfig
139
140        srcdir = self.get_temp_dir()
141        curdir = self.get_temp_dir()
142        dirs = [srcdir, curdir]
143        loader = MozconfigLoader(srcdir)
144
145        path = os.path.join(srcdir, relative_mozconfig)
146        with open(path, 'w'):
147            pass
148
149        orig_dir = os.getcwd()
150        try:
151            os.chdir(curdir)
152            self.assertEqual(os.path.normpath(loader.find_mozconfig()),
153                             os.path.normpath(path))
154        finally:
155            os.chdir(orig_dir)
156
157    def test_find_abs_path_not_exist(self):
158        """Ensure a missing absolute path is detected."""
159        os.environ[b'MOZCONFIG'] = '/foo/bar/does/not/exist'
160
161        with self.assertRaises(MozconfigFindException) as e:
162            self.get_loader().find_mozconfig()
163
164        self.assertIn('path that does not exist', e.exception.message)
165        self.assertTrue(e.exception.message.endswith('/foo/bar/does/not/exist'))
166
167    def test_find_path_not_file(self):
168        """Ensure non-file paths are detected."""
169
170        os.environ[b'MOZCONFIG'] = gettempdir()
171
172        with self.assertRaises(MozconfigFindException) as e:
173            self.get_loader().find_mozconfig()
174
175        self.assertIn('refers to a non-file', e.exception.message)
176        self.assertTrue(e.exception.message.endswith(gettempdir()))
177
178    def test_find_default_files(self):
179        """Ensure default paths are used when present."""
180        for p in MozconfigLoader.DEFAULT_TOPSRCDIR_PATHS:
181            d = self.get_temp_dir()
182            path = os.path.join(d, p)
183
184            with open(path, 'w'):
185                pass
186
187            self.assertEqual(MozconfigLoader(d).find_mozconfig(), path)
188
189    def test_find_multiple_defaults(self):
190        """Ensure we error when multiple default files are present."""
191        self.assertGreater(len(MozconfigLoader.DEFAULT_TOPSRCDIR_PATHS), 1)
192
193        d = self.get_temp_dir()
194        for p in MozconfigLoader.DEFAULT_TOPSRCDIR_PATHS:
195            with open(os.path.join(d, p), 'w'):
196                pass
197
198        with self.assertRaises(MozconfigFindException) as e:
199            MozconfigLoader(d).find_mozconfig()
200
201        self.assertIn('Multiple default mozconfig files present',
202            e.exception.message)
203
204    def test_find_deprecated_path_srcdir(self):
205        """Ensure we error when deprecated path locations are present."""
206        for p in MozconfigLoader.DEPRECATED_TOPSRCDIR_PATHS:
207            d = self.get_temp_dir()
208            with open(os.path.join(d, p), 'w'):
209                pass
210
211            with self.assertRaises(MozconfigFindException) as e:
212                MozconfigLoader(d).find_mozconfig()
213
214            self.assertIn('This implicit location is no longer',
215                e.exception.message)
216            self.assertIn(d, e.exception.message)
217
218    def test_find_deprecated_home_paths(self):
219        """Ensure we error when deprecated home directory paths are present."""
220
221        for p in MozconfigLoader.DEPRECATED_HOME_PATHS:
222            home = self.get_temp_dir()
223            os.environ[b'HOME'] = home
224            path = os.path.join(home, p)
225
226            with open(path, 'w'):
227                pass
228
229            with self.assertRaises(MozconfigFindException) as e:
230                self.get_loader().find_mozconfig()
231
232            self.assertIn('This implicit location is no longer',
233                e.exception.message)
234            self.assertIn(path, e.exception.message)
235
236    def test_read_no_mozconfig(self):
237        # This is basically to ensure changes to defaults incur a test failure.
238        result = self.get_loader().read_mozconfig()
239
240        self.assertEqual(result, {
241            'path': None,
242            'topobjdir': None,
243            'configure_args': None,
244            'make_flags': None,
245            'make_extra': None,
246            'env': None,
247            'vars': None,
248        })
249
250    def test_read_empty_mozconfig(self):
251        with NamedTemporaryFile(mode='w') as mozconfig:
252            result = self.get_loader().read_mozconfig(mozconfig.name)
253
254            self.assertEqual(result['path'], mozconfig.name)
255            self.assertIsNone(result['topobjdir'])
256            self.assertEqual(result['configure_args'], [])
257            self.assertEqual(result['make_flags'], [])
258            self.assertEqual(result['make_extra'], [])
259
260            for f in ('added', 'removed', 'modified'):
261                self.assertEqual(len(result['vars'][f]), 0)
262                self.assertEqual(len(result['env'][f]), 0)
263
264            self.assertEqual(result['env']['unmodified'], {})
265
266    def test_read_capture_ac_options(self):
267        """Ensures ac_add_options calls are captured."""
268        with NamedTemporaryFile(mode='w') as mozconfig:
269            mozconfig.write('ac_add_options --enable-debug\n')
270            mozconfig.write('ac_add_options --disable-tests --enable-foo\n')
271            mozconfig.write('ac_add_options --foo="bar baz"\n')
272            mozconfig.flush()
273
274            result = self.get_loader().read_mozconfig(mozconfig.name)
275            self.assertEqual(result['configure_args'], [
276                '--enable-debug', '--disable-tests', '--enable-foo',
277                '--foo=bar baz'])
278
279    def test_read_ac_options_substitution(self):
280        """Ensure ac_add_options values are substituted."""
281        with NamedTemporaryFile(mode='w') as mozconfig:
282            mozconfig.write('ac_add_options --foo=@TOPSRCDIR@\n')
283            mozconfig.flush()
284
285            loader = self.get_loader()
286            result = loader.read_mozconfig(mozconfig.name)
287            self.assertEqual(result['configure_args'], [
288                '--foo=%s' % loader.topsrcdir])
289
290    def test_read_capture_mk_options(self):
291        """Ensures mk_add_options calls are captured."""
292        with NamedTemporaryFile(mode='w') as mozconfig:
293            mozconfig.write('mk_add_options MOZ_OBJDIR=/foo/bar\n')
294            mozconfig.write('mk_add_options MOZ_MAKE_FLAGS="-j8 -s"\n')
295            mozconfig.write('mk_add_options FOO="BAR BAZ"\n')
296            mozconfig.write('mk_add_options BIZ=1\n')
297            mozconfig.flush()
298
299            result = self.get_loader().read_mozconfig(mozconfig.name)
300            self.assertEqual(result['topobjdir'], '/foo/bar')
301            self.assertEqual(result['make_flags'], ['-j8', '-s'])
302            self.assertEqual(result['make_extra'], ['FOO=BAR BAZ', 'BIZ=1'])
303
304    def test_read_empty_mozconfig_objdir_environ(self):
305        os.environ[b'MOZ_OBJDIR'] = b'obj-firefox'
306        with NamedTemporaryFile(mode='w') as mozconfig:
307            result = self.get_loader().read_mozconfig(mozconfig.name)
308            self.assertEqual(result['topobjdir'], 'obj-firefox')
309
310    def test_read_capture_mk_options_objdir_environ(self):
311        """Ensures mk_add_options calls are captured and override the environ."""
312        os.environ[b'MOZ_OBJDIR'] = b'obj-firefox'
313        with NamedTemporaryFile(mode='w') as mozconfig:
314            mozconfig.write('mk_add_options MOZ_OBJDIR=/foo/bar\n')
315            mozconfig.flush()
316
317            result = self.get_loader().read_mozconfig(mozconfig.name)
318            self.assertEqual(result['topobjdir'], '/foo/bar')
319
320    def test_read_moz_objdir_substitution(self):
321        """Ensure @TOPSRCDIR@ substitution is recognized in MOZ_OBJDIR."""
322        with NamedTemporaryFile(mode='w') as mozconfig:
323            mozconfig.write('mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/some-objdir')
324            mozconfig.flush()
325
326            loader = self.get_loader()
327            result = loader.read_mozconfig(mozconfig.name)
328
329            self.assertEqual(result['topobjdir'], '%s/some-objdir' %
330                loader.topsrcdir)
331
332    def test_read_new_variables(self):
333        """New variables declared in mozconfig file are detected."""
334        with NamedTemporaryFile(mode='w') as mozconfig:
335            mozconfig.write('CC=/usr/local/bin/clang\n')
336            mozconfig.write('CXX=/usr/local/bin/clang++\n')
337            mozconfig.flush()
338
339            result = self.get_loader().read_mozconfig(mozconfig.name)
340
341            self.assertEqual(result['vars']['added'], {
342                'CC': '/usr/local/bin/clang',
343                'CXX': '/usr/local/bin/clang++'})
344            self.assertEqual(result['env']['added'], {})
345
346    def test_read_exported_variables(self):
347        """Exported variables are caught as new variables."""
348        with NamedTemporaryFile(mode='w') as mozconfig:
349            mozconfig.write('export MY_EXPORTED=woot\n')
350            mozconfig.flush()
351
352            result = self.get_loader().read_mozconfig(mozconfig.name)
353
354            self.assertEqual(result['vars']['added'], {})
355            self.assertEqual(result['env']['added'], {
356                'MY_EXPORTED': 'woot'})
357
358    def test_read_modify_variables(self):
359        """Variables modified by mozconfig are detected."""
360        old_path = os.path.realpath(b'/usr/bin/gcc')
361        new_path = os.path.realpath(b'/usr/local/bin/clang')
362        os.environ[b'CC'] = old_path
363
364        with NamedTemporaryFile(mode='w') as mozconfig:
365            mozconfig.write('CC="%s"\n' % new_path)
366            mozconfig.flush()
367
368            result = self.get_loader().read_mozconfig(mozconfig.name)
369
370            self.assertEqual(result['vars']['modified'], {})
371            self.assertEqual(result['env']['modified'], {
372                'CC': (old_path, new_path)
373            })
374
375    def test_read_unmodified_variables(self):
376        """Variables modified by mozconfig are detected."""
377        cc_path = os.path.realpath(b'/usr/bin/gcc')
378        os.environ[b'CC'] = cc_path
379
380        with NamedTemporaryFile(mode='w') as mozconfig:
381            mozconfig.flush()
382
383            result = self.get_loader().read_mozconfig(mozconfig.name)
384
385            self.assertEqual(result['vars']['unmodified'], {})
386            self.assertEqual(result['env']['unmodified'], {
387                'CC': cc_path
388            })
389
390    def test_read_removed_variables(self):
391        """Variables unset by the mozconfig are detected."""
392        cc_path = os.path.realpath(b'/usr/bin/clang')
393        os.environ[b'CC'] = cc_path
394
395        with NamedTemporaryFile(mode='w') as mozconfig:
396            mozconfig.write('unset CC\n')
397            mozconfig.flush()
398
399            result = self.get_loader().read_mozconfig(mozconfig.name)
400
401            self.assertEqual(result['vars']['removed'], {})
402            self.assertEqual(result['env']['removed'], {
403                'CC': cc_path})
404
405    def test_read_multiline_variables(self):
406        """Ensure multi-line variables are captured properly."""
407        with NamedTemporaryFile(mode='w') as mozconfig:
408            mozconfig.write('multi="foo\nbar"\n')
409            mozconfig.write('single=1\n')
410            mozconfig.flush()
411
412            result = self.get_loader().read_mozconfig(mozconfig.name)
413
414            self.assertEqual(result['vars']['added'], {
415                'multi': 'foo\nbar',
416                'single': '1'
417            })
418            self.assertEqual(result['env']['added'], {})
419
420    def test_read_topsrcdir_defined(self):
421        """Ensure $topsrcdir references work as expected."""
422        with NamedTemporaryFile(mode='w') as mozconfig:
423            mozconfig.write('TEST=$topsrcdir')
424            mozconfig.flush()
425
426            loader = self.get_loader()
427            result = loader.read_mozconfig(mozconfig.name)
428
429            self.assertEqual(result['vars']['added']['TEST'],
430                loader.topsrcdir.replace(os.sep, '/'))
431            self.assertEqual(result['env']['added'], {})
432
433    def test_read_empty_variable_value(self):
434        """Ensure empty variable values are parsed properly."""
435        with NamedTemporaryFile(mode='w') as mozconfig:
436            mozconfig.write('EMPTY=\n')
437            mozconfig.write('export EXPORT_EMPTY=\n')
438            mozconfig.flush()
439
440            result = self.get_loader().read_mozconfig(mozconfig.name)
441
442            self.assertEqual(result['vars']['added'], {
443                'EMPTY': '',
444            })
445            self.assertEqual(result['env']['added'], {
446                'EXPORT_EMPTY': ''
447            })
448
449    def test_read_load_exception(self):
450        """Ensure non-0 exit codes in mozconfigs are handled properly."""
451        with NamedTemporaryFile(mode='w') as mozconfig:
452            mozconfig.write('echo "hello world"\n')
453            mozconfig.write('exit 1\n')
454            mozconfig.flush()
455
456            with self.assertRaises(MozconfigLoadException) as e:
457                self.get_loader().read_mozconfig(mozconfig.name)
458
459            self.assertTrue(e.exception.message.startswith(
460                'Evaluation of your mozconfig exited with an error'))
461            self.assertEquals(e.exception.path,
462                mozconfig.name.replace(os.sep, '/'))
463            self.assertEquals(e.exception.output, ['hello world'])
464
465
466if __name__ == '__main__':
467    main()
468