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 contextlib
8import os
9import sys
10import textwrap
11import traceback
12import unittest
13
14from mozunit import (
15    main,
16    MockedOpen,
17)
18
19from mozbuild.configure import ConfigureError
20from mozbuild.configure.lint import LintSandbox
21
22import mozpack.path as mozpath
23
24test_data_path = mozpath.abspath(mozpath.dirname(__file__))
25test_data_path = mozpath.join(test_data_path, "data")
26
27
28class TestLint(unittest.TestCase):
29    def lint_test(self, options=[], env={}):
30        sandbox = LintSandbox(env, ["configure"] + options)
31
32        sandbox.run(mozpath.join(test_data_path, "moz.configure"))
33
34    def moz_configure(self, source):
35        return MockedOpen(
36            {os.path.join(test_data_path, "moz.configure"): textwrap.dedent(source)}
37        )
38
39    @contextlib.contextmanager
40    def assertRaisesFromLine(self, exc_type, line):
41        with self.assertRaises(exc_type) as e:
42            yield e
43
44        _, _, tb = sys.exc_info()
45        self.assertEquals(
46            traceback.extract_tb(tb)[-1][:2],
47            (mozpath.join(test_data_path, "moz.configure"), line),
48        )
49
50    def test_configure_testcase(self):
51        # Lint python/mozbuild/mozbuild/test/configure/data/moz.configure
52        self.lint_test()
53
54    def test_depends_failures(self):
55        with self.moz_configure(
56            """
57            option('--foo', help='foo')
58            @depends('--foo')
59            def foo(value):
60                return value
61
62            @depends('--help', foo)
63            @imports('os')
64            def bar(help, foo):
65                return foo
66        """
67        ):
68            self.lint_test()
69
70        with self.assertRaisesFromLine(ConfigureError, 7) as e:
71            with self.moz_configure(
72                """
73                option('--foo', help='foo')
74                @depends('--foo')
75                def foo(value):
76                    return value
77
78                @depends('--help', foo)
79                def bar(help, foo):
80                    return foo
81            """
82            ):
83                self.lint_test()
84
85        self.assertEquals(str(e.exception), "The dependency on `--help` is unused")
86
87        with self.assertRaisesFromLine(ConfigureError, 3) as e:
88            with self.moz_configure(
89                """
90                option('--foo', help='foo')
91                @depends('--foo')
92                @imports('os')
93                def foo(value):
94                    return value
95
96                @depends('--help', foo)
97                @imports('os')
98                def bar(help, foo):
99                    return foo
100            """
101            ):
102                self.lint_test()
103
104        self.assertEquals(
105            str(e.exception),
106            "Missing '--help' dependency because `bar` depends on '--help' and `foo`",
107        )
108
109        with self.assertRaisesFromLine(ConfigureError, 7) as e:
110            with self.moz_configure(
111                """
112                @template
113                def tmpl():
114                    qux = 42
115
116                    option('--foo', help='foo')
117                    @depends('--foo')
118                    def foo(value):
119                        qux
120                        return value
121
122                    @depends('--help', foo)
123                    @imports('os')
124                    def bar(help, foo):
125                        return foo
126                tmpl()
127            """
128            ):
129                self.lint_test()
130
131        self.assertEquals(
132            str(e.exception),
133            "Missing '--help' dependency because `bar` depends on '--help' and `foo`",
134        )
135
136        with self.moz_configure(
137            """
138            option('--foo', help='foo')
139            @depends('--foo')
140            def foo(value):
141                return value
142
143            include(foo)
144        """
145        ):
146            self.lint_test()
147
148        with self.assertRaisesFromLine(ConfigureError, 3) as e:
149            with self.moz_configure(
150                """
151                option('--foo', help='foo')
152                @depends('--foo')
153                @imports('os')
154                def foo(value):
155                    return value
156
157                include(foo)
158            """
159            ):
160                self.lint_test()
161
162        self.assertEquals(str(e.exception), "Missing '--help' dependency")
163
164        with self.assertRaisesFromLine(ConfigureError, 3) as e:
165            with self.moz_configure(
166                """
167                option('--foo', help='foo')
168                @depends('--foo')
169                @imports('os')
170                def foo(value):
171                    return value
172
173                @depends(foo)
174                def bar(value):
175                    return value
176
177                include(bar)
178            """
179            ):
180                self.lint_test()
181
182        self.assertEquals(str(e.exception), "Missing '--help' dependency")
183
184        with self.assertRaisesFromLine(ConfigureError, 3) as e:
185            with self.moz_configure(
186                """
187                option('--foo', help='foo')
188                @depends('--foo')
189                @imports('os')
190                def foo(value):
191                    return value
192
193                option('--bar', help='bar', when=foo)
194            """
195            ):
196                self.lint_test()
197
198        self.assertEquals(str(e.exception), "Missing '--help' dependency")
199
200        # This would have failed with "Missing '--help' dependency"
201        # in the past, because of the reference to the builtin False.
202        with self.moz_configure(
203            """
204            option('--foo', help='foo')
205            @depends('--foo')
206            def foo(value):
207                return False or value
208
209            option('--bar', help='bar', when=foo)
210        """
211        ):
212            self.lint_test()
213
214        # However, when something that is normally a builtin is overridden,
215        # we should still want the dependency on --help.
216        with self.assertRaisesFromLine(ConfigureError, 7) as e:
217            with self.moz_configure(
218                """
219                @template
220                def tmpl():
221                    sorted = 42
222
223                    option('--foo', help='foo')
224                    @depends('--foo')
225                    def foo(value):
226                        return sorted
227
228                    option('--bar', help='bar', when=foo)
229                tmpl()
230            """
231            ):
232                self.lint_test()
233
234        self.assertEquals(str(e.exception), "Missing '--help' dependency")
235
236        # There is a default restricted `os` module when there is no explicit
237        # @imports, and it's fine to use it without a dependency on --help.
238        with self.moz_configure(
239            """
240            option('--foo', help='foo')
241            @depends('--foo')
242            def foo(value):
243                os
244                return value
245
246            include(foo)
247        """
248        ):
249            self.lint_test()
250
251        with self.assertRaisesFromLine(ConfigureError, 3) as e:
252            with self.moz_configure(
253                """
254                option('--foo', help='foo')
255                @depends('--foo')
256                def foo(value):
257                    return
258
259                include(foo)
260            """
261            ):
262                self.lint_test()
263
264        self.assertEquals(str(e.exception), "The dependency on `--foo` is unused")
265
266        with self.assertRaisesFromLine(ConfigureError, 5) as e:
267            with self.moz_configure(
268                """
269                @depends(when=True)
270                def bar():
271                    return
272                @depends(bar)
273                def foo(value):
274                    return
275
276                include(foo)
277            """
278            ):
279                self.lint_test()
280
281        self.assertEquals(str(e.exception), "The dependency on `bar` is unused")
282
283        with self.assertRaisesFromLine(ConfigureError, 2) as e:
284            with self.moz_configure(
285                """
286                @depends(depends(when=True)(lambda: None))
287                def foo(value):
288                    return
289
290                include(foo)
291            """
292            ):
293                self.lint_test()
294
295        self.assertEquals(str(e.exception), "The dependency on `<lambda>` is unused")
296
297        with self.assertRaisesFromLine(ConfigureError, 9) as e:
298            with self.moz_configure(
299                """
300                @template
301                def tmpl():
302                    @depends(when=True)
303                    def bar():
304                        return
305                    return bar
306                qux = tmpl()
307                @depends(qux)
308                def foo(value):
309                    return
310
311                include(foo)
312            """
313            ):
314                self.lint_test()
315
316        self.assertEquals(str(e.exception), "The dependency on `qux` is unused")
317
318    def test_default_enable(self):
319        # --enable-* with default=True is not allowed.
320        with self.moz_configure(
321            """
322            option('--enable-foo', default=False, help='foo')
323        """
324        ):
325            self.lint_test()
326        with self.assertRaisesFromLine(ConfigureError, 2) as e:
327            with self.moz_configure(
328                """
329                option('--enable-foo', default=True, help='foo')
330            """
331            ):
332                self.lint_test()
333        self.assertEquals(
334            str(e.exception),
335            "--disable-foo should be used instead of " "--enable-foo with default=True",
336        )
337
338    def test_default_disable(self):
339        # --disable-* with default=False is not allowed.
340        with self.moz_configure(
341            """
342            option('--disable-foo', default=True, help='foo')
343        """
344        ):
345            self.lint_test()
346        with self.assertRaisesFromLine(ConfigureError, 2) as e:
347            with self.moz_configure(
348                """
349                option('--disable-foo', default=False, help='foo')
350            """
351            ):
352                self.lint_test()
353        self.assertEquals(
354            str(e.exception),
355            "--enable-foo should be used instead of "
356            "--disable-foo with default=False",
357        )
358
359    def test_default_with(self):
360        # --with-* with default=True is not allowed.
361        with self.moz_configure(
362            """
363            option('--with-foo', default=False, help='foo')
364        """
365        ):
366            self.lint_test()
367        with self.assertRaisesFromLine(ConfigureError, 2) as e:
368            with self.moz_configure(
369                """
370                option('--with-foo', default=True, help='foo')
371            """
372            ):
373                self.lint_test()
374        self.assertEquals(
375            str(e.exception),
376            "--without-foo should be used instead of " "--with-foo with default=True",
377        )
378
379    def test_default_without(self):
380        # --without-* with default=False is not allowed.
381        with self.moz_configure(
382            """
383            option('--without-foo', default=True, help='foo')
384        """
385        ):
386            self.lint_test()
387        with self.assertRaisesFromLine(ConfigureError, 2) as e:
388            with self.moz_configure(
389                """
390                option('--without-foo', default=False, help='foo')
391            """
392            ):
393                self.lint_test()
394        self.assertEquals(
395            str(e.exception),
396            "--with-foo should be used instead of " "--without-foo with default=False",
397        )
398
399    def test_default_func(self):
400        # Help text for an option with variable default should contain
401        # {enable|disable} rule.
402        with self.moz_configure(
403            """
404            option(env='FOO', help='foo')
405            option('--enable-bar', default=depends('FOO')(lambda x: bool(x)),
406                   help='{Enable|Disable} bar')
407        """
408        ):
409            self.lint_test()
410        with self.assertRaisesFromLine(ConfigureError, 3) as e:
411            with self.moz_configure(
412                """
413                option(env='FOO', help='foo')
414                option('--enable-bar', default=depends('FOO')(lambda x: bool(x)),\
415                       help='Enable bar')
416            """
417            ):
418                self.lint_test()
419        self.assertEquals(
420            str(e.exception),
421            '`help` should contain "{Enable|Disable}" because of '
422            "non-constant default",
423        )
424
425    def test_large_offset(self):
426        with self.assertRaisesFromLine(ConfigureError, 375):
427            with self.moz_configure(
428                """
429                option(env='FOO', help='foo')
430            """
431                + "\n" * 371
432                + """
433                option('--enable-bar', default=depends('FOO')(lambda x: bool(x)),\
434                       help='Enable bar')
435            """
436            ):
437                self.lint_test()
438
439    def test_undefined_global(self):
440        with self.assertRaisesFromLine(NameError, 6) as e:
441            with self.moz_configure(
442                """
443                option(env='FOO', help='foo')
444                @depends('FOO')
445                def foo(value):
446                    if value:
447                        return unknown
448                    return value
449            """
450            ):
451                self.lint_test()
452
453        self.assertEquals(str(e.exception), "global name 'unknown' is not defined")
454
455        # Ideally, this would raise on line 4, where `unknown` is used, but
456        # python disassembly doesn't give use the information.
457        with self.assertRaisesFromLine(NameError, 2) as e:
458            with self.moz_configure(
459                """
460                @template
461                def tmpl():
462                    @depends(unknown)
463                    def foo(value):
464                        if value:
465                            return True
466                    return foo
467                tmpl()
468            """
469            ):
470                self.lint_test()
471
472        self.assertEquals(str(e.exception), "global name 'unknown' is not defined")
473
474    def test_unnecessary_imports(self):
475        with self.assertRaisesFromLine(NameError, 3) as e:
476            with self.moz_configure(
477                """
478                option(env='FOO', help='foo')
479                @depends('FOO')
480                @imports(_from='__builtin__', _import='list')
481                def foo(value):
482                    if value:
483                        return list()
484                    return value
485            """
486            ):
487                self.lint_test()
488
489        self.assertEquals(
490            str(e.exception), "builtin 'list' doesn't need to be imported"
491        )
492
493
494if __name__ == "__main__":
495    main()
496