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
7from six import StringIO
8import os
9import six
10import sys
11import textwrap
12import unittest
13
14from mozunit import (
15    main,
16    MockedOpen,
17)
18
19from mozbuild.configure.options import (
20    InvalidOptionError,
21    NegativeOptionValue,
22    PositiveOptionValue,
23)
24from mozbuild.configure import (
25    ConfigureError,
26    ConfigureSandbox,
27)
28from mozbuild.util import exec_, memoized_property, ReadOnlyNamespace
29
30import mozpack.path as mozpath
31
32test_data_path = mozpath.abspath(mozpath.dirname(__file__))
33test_data_path = mozpath.join(test_data_path, "data")
34
35
36class TestConfigure(unittest.TestCase):
37    def get_config(
38        self, options=[], env={}, configure="moz.configure", prog="/bin/configure"
39    ):
40        config = {}
41        out = StringIO()
42        sandbox = ConfigureSandbox(config, env, [prog] + options, out, out)
43
44        sandbox.run(mozpath.join(test_data_path, configure))
45
46        if "--help" in options:
47            return six.ensure_text(out.getvalue()), config
48        self.assertEqual("", out.getvalue())
49        return config
50
51    def moz_configure(self, source):
52        return MockedOpen(
53            {os.path.join(test_data_path, "moz.configure"): textwrap.dedent(source)}
54        )
55
56    def test_defaults(self):
57        config = self.get_config()
58        self.maxDiff = None
59        self.assertEqual(
60            {
61                "CHOICES": NegativeOptionValue(),
62                "DEFAULTED": PositiveOptionValue(("not-simple",)),
63                "IS_GCC": NegativeOptionValue(),
64                "REMAINDER": (
65                    PositiveOptionValue(),
66                    NegativeOptionValue(),
67                    NegativeOptionValue(),
68                    NegativeOptionValue(),
69                ),
70                "SIMPLE": NegativeOptionValue(),
71                "VALUES": NegativeOptionValue(),
72                "VALUES2": NegativeOptionValue(),
73                "VALUES3": NegativeOptionValue(),
74                "WITH_ENV": NegativeOptionValue(),
75            },
76            config,
77        )
78
79    def test_help(self):
80        help, config = self.get_config(["--help"], prog="configure")
81
82        self.assertEqual({}, config)
83        self.maxDiff = None
84        self.assertEqual(
85            "Usage: configure [options]\n"
86            "\n"
87            "Options: [defaults in brackets after descriptions]\n"
88            "  Help options:\n"
89            "    --help                    print this message\n"
90            "\n"
91            "  Options from python/mozbuild/mozbuild/test/configure/data/included.configure:\n"
92            "    --enable-imports-in-template\n                              Imports in template\n"
93            "\n"
94            "  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n"
95            "    --enable-include          Include\n"
96            "    --enable-simple           Enable simple\n"
97            "    --enable-values           Enable values\n"
98            "    --enable-with-env         Enable with env\n"
99            "    --indirect-option         Indirectly defined option\n"
100            "    --option                  Option\n"
101            "    --returned-choices        Choices\n"
102            "    --with-imports            Imports\n"
103            "    --with-returned-default   Returned default [not-simple]\n"
104            "    --with-stuff              Build with stuff\n"
105            "    --without-thing           Build without thing\n"
106            "\n"
107            "\n"
108            "Environment variables:\n"
109            "  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n"
110            "    CC                        C Compiler\n"
111            "\n",
112            help.replace("\\", "/"),
113        )
114
115    def test_unknown(self):
116        with self.assertRaises(InvalidOptionError):
117            self.get_config(["--unknown"])
118
119    def test_simple(self):
120        for config in (
121            self.get_config(),
122            self.get_config(["--disable-simple"]),
123            # Last option wins.
124            self.get_config(["--enable-simple", "--disable-simple"]),
125        ):
126            self.assertNotIn("ENABLED_SIMPLE", config)
127            self.assertIn("SIMPLE", config)
128            self.assertEqual(NegativeOptionValue(), config["SIMPLE"])
129
130        for config in (
131            self.get_config(["--enable-simple"]),
132            self.get_config(["--disable-simple", "--enable-simple"]),
133        ):
134            self.assertIn("ENABLED_SIMPLE", config)
135            self.assertIn("SIMPLE", config)
136            self.assertEqual(PositiveOptionValue(), config["SIMPLE"])
137            self.assertIs(config["SIMPLE"], config["ENABLED_SIMPLE"])
138
139        # --enable-simple doesn't take values.
140        with self.assertRaises(InvalidOptionError):
141            self.get_config(["--enable-simple=value"])
142
143    def test_with_env(self):
144        for config in (
145            self.get_config(),
146            self.get_config(["--disable-with-env"]),
147            self.get_config(["--enable-with-env", "--disable-with-env"]),
148            self.get_config(env={"MOZ_WITH_ENV": ""}),
149            # Options win over environment
150            self.get_config(["--disable-with-env"], env={"MOZ_WITH_ENV": "1"}),
151        ):
152            self.assertIn("WITH_ENV", config)
153            self.assertEqual(NegativeOptionValue(), config["WITH_ENV"])
154
155        for config in (
156            self.get_config(["--enable-with-env"]),
157            self.get_config(["--disable-with-env", "--enable-with-env"]),
158            self.get_config(env={"MOZ_WITH_ENV": "1"}),
159            self.get_config(["--enable-with-env"], env={"MOZ_WITH_ENV": ""}),
160        ):
161            self.assertIn("WITH_ENV", config)
162            self.assertEqual(PositiveOptionValue(), config["WITH_ENV"])
163
164        with self.assertRaises(InvalidOptionError):
165            self.get_config(["--enable-with-env=value"])
166
167        with self.assertRaises(InvalidOptionError):
168            self.get_config(env={"MOZ_WITH_ENV": "value"})
169
170    def test_values(self, name="VALUES"):
171        for config in (
172            self.get_config(),
173            self.get_config(["--disable-values"]),
174            self.get_config(["--enable-values", "--disable-values"]),
175        ):
176            self.assertIn(name, config)
177            self.assertEqual(NegativeOptionValue(), config[name])
178
179        for config in (
180            self.get_config(["--enable-values"]),
181            self.get_config(["--disable-values", "--enable-values"]),
182        ):
183            self.assertIn(name, config)
184            self.assertEqual(PositiveOptionValue(), config[name])
185
186        config = self.get_config(["--enable-values=foo"])
187        self.assertIn(name, config)
188        self.assertEqual(PositiveOptionValue(("foo",)), config[name])
189
190        config = self.get_config(["--enable-values=foo,bar"])
191        self.assertIn(name, config)
192        self.assertTrue(config[name])
193        self.assertEqual(PositiveOptionValue(("foo", "bar")), config[name])
194
195    def test_values2(self):
196        self.test_values("VALUES2")
197
198    def test_values3(self):
199        self.test_values("VALUES3")
200
201    def test_returned_default(self):
202        config = self.get_config(["--enable-simple"])
203        self.assertIn("DEFAULTED", config)
204        self.assertEqual(PositiveOptionValue(("simple",)), config["DEFAULTED"])
205
206        config = self.get_config(["--disable-simple"])
207        self.assertIn("DEFAULTED", config)
208        self.assertEqual(PositiveOptionValue(("not-simple",)), config["DEFAULTED"])
209
210    def test_returned_choices(self):
211        for val in ("a", "b", "c"):
212            config = self.get_config(
213                ["--enable-values=alpha", "--returned-choices=%s" % val]
214            )
215            self.assertIn("CHOICES", config)
216            self.assertEqual(PositiveOptionValue((val,)), config["CHOICES"])
217
218        for val in ("0", "1", "2"):
219            config = self.get_config(
220                ["--enable-values=numeric", "--returned-choices=%s" % val]
221            )
222            self.assertIn("CHOICES", config)
223            self.assertEqual(PositiveOptionValue((val,)), config["CHOICES"])
224
225        with self.assertRaises(InvalidOptionError):
226            self.get_config(["--enable-values=numeric", "--returned-choices=a"])
227
228        with self.assertRaises(InvalidOptionError):
229            self.get_config(["--enable-values=alpha", "--returned-choices=0"])
230
231    def test_included(self):
232        config = self.get_config(env={"CC": "gcc"})
233        self.assertIn("IS_GCC", config)
234        self.assertEqual(config["IS_GCC"], True)
235
236        config = self.get_config(["--enable-include=extra.configure", "--extra"])
237        self.assertIn("EXTRA", config)
238        self.assertEqual(PositiveOptionValue(), config["EXTRA"])
239
240        with self.assertRaises(InvalidOptionError):
241            self.get_config(["--extra"])
242
243    def test_template(self):
244        config = self.get_config(env={"CC": "gcc"})
245        self.assertIn("CFLAGS", config)
246        self.assertEqual(config["CFLAGS"], ["-Werror=foobar"])
247
248        config = self.get_config(env={"CC": "clang"})
249        self.assertNotIn("CFLAGS", config)
250
251    def test_imports(self):
252        config = {}
253        out = StringIO()
254        sandbox = ConfigureSandbox(config, {}, ["configure"], out, out)
255
256        with self.assertRaises(ImportError):
257            exec_(
258                textwrap.dedent(
259                    """
260                @template
261                def foo():
262                    import sys
263                foo()"""
264                ),
265                sandbox,
266            )
267
268        exec_(
269            textwrap.dedent(
270                """
271            @template
272            @imports('sys')
273            def foo():
274                return sys"""
275            ),
276            sandbox,
277        )
278
279        self.assertIs(sandbox["foo"](), sys)
280
281        # os.path after an import is a mix of vanilla os.path and sandbox os.path.
282        os_path = {}
283        exec_("from os.path import *", {}, os_path)
284        os_path.update(sandbox.OS.path.__dict__)
285        os_path = ReadOnlyNamespace(**os_path)
286
287        exec_(
288            textwrap.dedent(
289                """
290            @template
291            @imports(_from='os', _import='path')
292            def foo():
293                return path"""
294            ),
295            sandbox,
296        )
297
298        self.assertEqual(sandbox["foo"](), os_path)
299
300        exec_(
301            textwrap.dedent(
302                """
303            @template
304            @imports(_from='os', _import='path', _as='os_path')
305            def foo():
306                return os_path"""
307            ),
308            sandbox,
309        )
310
311        self.assertEqual(sandbox["foo"](), os_path)
312
313        exec_(
314            textwrap.dedent(
315                """
316            @template
317            @imports('__builtin__')
318            def foo():
319                return __builtin__"""
320            ),
321            sandbox,
322        )
323
324        self.assertIs(sandbox["foo"](), six.moves.builtins)
325
326        exec_(
327            textwrap.dedent(
328                """
329            @template
330            @imports(_from='__builtin__', _import='open')
331            def foo():
332                return open('%s')"""
333                % os.devnull
334            ),
335            sandbox,
336        )
337
338        f = sandbox["foo"]()
339        self.assertEqual(f.name, os.devnull)
340        f.close()
341
342        # This unlocks the sandbox
343        exec_(
344            textwrap.dedent(
345                """
346            @template
347            @imports(_import='__builtin__', _as='__builtins__')
348            def foo():
349                import sys
350                return sys"""
351            ),
352            sandbox,
353        )
354
355        self.assertIs(sandbox["foo"](), sys)
356
357        exec_(
358            textwrap.dedent(
359                """
360            @template
361            @imports('__sandbox__')
362            def foo():
363                return __sandbox__"""
364            ),
365            sandbox,
366        )
367
368        self.assertIs(sandbox["foo"](), sandbox)
369
370        exec_(
371            textwrap.dedent(
372                """
373            @template
374            @imports(_import='__sandbox__', _as='s')
375            def foo():
376                return s"""
377            ),
378            sandbox,
379        )
380
381        self.assertIs(sandbox["foo"](), sandbox)
382
383        # Nothing leaked from the function being executed
384        self.assertEqual(list(sandbox), ["__builtins__", "foo"])
385        self.assertEqual(sandbox["__builtins__"], ConfigureSandbox.BUILTINS)
386
387        exec_(
388            textwrap.dedent(
389                """
390            @template
391            @imports('sys')
392            def foo():
393                @depends(when=True)
394                def bar():
395                    return sys
396                return bar
397            bar = foo()"""
398            ),
399            sandbox,
400        )
401
402        with self.assertRaises(NameError) as e:
403            sandbox._depends[sandbox["bar"]].result()
404
405        self.assertIn("name 'sys' is not defined", str(e.exception))
406
407    def test_apply_imports(self):
408        imports = []
409
410        class CountApplyImportsSandbox(ConfigureSandbox):
411            def _apply_imports(self, *args, **kwargs):
412                imports.append((args, kwargs))
413                super(CountApplyImportsSandbox, self)._apply_imports(*args, **kwargs)
414
415        config = {}
416        out = StringIO()
417        sandbox = CountApplyImportsSandbox(config, {}, ["configure"], out, out)
418
419        exec_(
420            textwrap.dedent(
421                """
422            @template
423            @imports('sys')
424            def foo():
425                return sys
426            foo()
427            foo()"""
428            ),
429            sandbox,
430        )
431
432        self.assertEqual(len(imports), 1)
433
434    def test_import_wrapping(self):
435        bar = object()
436        foo = ReadOnlyNamespace(bar=bar)
437
438        class BasicWrappingSandbox(ConfigureSandbox):
439            @memoized_property
440            def _wrapped_foo(self):
441                return foo
442
443        config = {}
444        out = StringIO()
445        sandbox = BasicWrappingSandbox(config, {}, ["configure"], out, out)
446
447        exec_(
448            textwrap.dedent(
449                """
450            @template
451            @imports('foo')
452            def toplevel():
453                return foo
454            @template
455            @imports('foo.bar')
456            def bar():
457                return foo.bar
458            @template
459            @imports('foo.bar')
460            def bar_upper():
461                return foo
462            @template
463            @imports(_from='foo', _import='bar')
464            def from_import():
465                return bar
466            @template
467            @imports(_from='foo', _import='bar', _as='custom_name')
468            def from_import_as():
469                return custom_name
470            @template
471            @imports(_import='foo', _as='custom_name')
472            def import_as():
473                return custom_name
474            """
475            ),
476            sandbox,
477        )
478        self.assertIs(sandbox["toplevel"](), foo)
479        self.assertIs(sandbox["bar"](), bar)
480        self.assertIs(sandbox["bar_upper"](), foo)
481        self.assertIs(sandbox["from_import"](), bar)
482        self.assertIs(sandbox["from_import_as"](), bar)
483        self.assertIs(sandbox["import_as"](), foo)
484
485    def test_os_path(self):
486        config = self.get_config(["--with-imports=%s" % __file__])
487        self.assertIn("HAS_ABSPATH", config)
488        self.assertEqual(config["HAS_ABSPATH"], True)
489        self.assertIn("HAS_GETATIME", config)
490        self.assertEqual(config["HAS_GETATIME"], True)
491        self.assertIn("HAS_GETATIME2", config)
492        self.assertEqual(config["HAS_GETATIME2"], False)
493
494    def test_template_call(self):
495        config = self.get_config(env={"CC": "gcc"})
496        self.assertIn("TEMPLATE_VALUE", config)
497        self.assertEqual(config["TEMPLATE_VALUE"], 42)
498        self.assertIn("TEMPLATE_VALUE_2", config)
499        self.assertEqual(config["TEMPLATE_VALUE_2"], 21)
500
501    def test_template_imports(self):
502        config = self.get_config(["--enable-imports-in-template"])
503        self.assertIn("PLATFORM", config)
504        self.assertEqual(config["PLATFORM"], sys.platform)
505
506    def test_decorators(self):
507        config = {}
508        out = StringIO()
509        sandbox = ConfigureSandbox(config, {}, ["configure"], out, out)
510
511        sandbox.include_file(mozpath.join(test_data_path, "decorators.configure"))
512
513        self.assertNotIn("FOO", sandbox)
514        self.assertNotIn("BAR", sandbox)
515        self.assertNotIn("QUX", sandbox)
516
517    def test_set_config(self):
518        def get_config(*args):
519            return self.get_config(*args, configure="set_config.configure")
520
521        help, config = get_config(["--help"])
522        self.assertEqual(config, {})
523
524        config = get_config(["--set-foo"])
525        self.assertIn("FOO", config)
526        self.assertEqual(config["FOO"], True)
527
528        config = get_config(["--set-bar"])
529        self.assertNotIn("FOO", config)
530        self.assertIn("BAR", config)
531        self.assertEqual(config["BAR"], True)
532
533        config = get_config(["--set-value=qux"])
534        self.assertIn("VALUE", config)
535        self.assertEqual(config["VALUE"], "qux")
536
537        config = get_config(["--set-name=hoge"])
538        self.assertIn("hoge", config)
539        self.assertEqual(config["hoge"], True)
540
541        config = get_config([])
542        self.assertEqual(config, {"BAR": False})
543
544        with self.assertRaises(ConfigureError):
545            # Both --set-foo and --set-name=FOO are going to try to
546            # set_config('FOO'...)
547            get_config(["--set-foo", "--set-name=FOO"])
548
549    def test_set_config_when(self):
550        with self.moz_configure(
551            """
552            option('--with-qux', help='qux')
553            set_config('FOO', 'foo', when=True)
554            set_config('BAR', 'bar', when=False)
555            set_config('QUX', 'qux', when='--with-qux')
556        """
557        ):
558            config = self.get_config()
559            self.assertEqual(
560                config,
561                {
562                    "FOO": "foo",
563                },
564            )
565            config = self.get_config(["--with-qux"])
566            self.assertEqual(
567                config,
568                {
569                    "FOO": "foo",
570                    "QUX": "qux",
571                },
572            )
573
574    def test_set_define(self):
575        def get_config(*args):
576            return self.get_config(*args, configure="set_define.configure")
577
578        help, config = get_config(["--help"])
579        self.assertEqual(config, {"DEFINES": {}})
580
581        config = get_config(["--set-foo"])
582        self.assertIn("FOO", config["DEFINES"])
583        self.assertEqual(config["DEFINES"]["FOO"], True)
584
585        config = get_config(["--set-bar"])
586        self.assertNotIn("FOO", config["DEFINES"])
587        self.assertIn("BAR", config["DEFINES"])
588        self.assertEqual(config["DEFINES"]["BAR"], True)
589
590        config = get_config(["--set-value=qux"])
591        self.assertIn("VALUE", config["DEFINES"])
592        self.assertEqual(config["DEFINES"]["VALUE"], "qux")
593
594        config = get_config(["--set-name=hoge"])
595        self.assertIn("hoge", config["DEFINES"])
596        self.assertEqual(config["DEFINES"]["hoge"], True)
597
598        config = get_config([])
599        self.assertEqual(config["DEFINES"], {"BAR": False})
600
601        with self.assertRaises(ConfigureError):
602            # Both --set-foo and --set-name=FOO are going to try to
603            # set_define('FOO'...)
604            get_config(["--set-foo", "--set-name=FOO"])
605
606    def test_set_define_when(self):
607        with self.moz_configure(
608            """
609            option('--with-qux', help='qux')
610            set_define('FOO', 'foo', when=True)
611            set_define('BAR', 'bar', when=False)
612            set_define('QUX', 'qux', when='--with-qux')
613        """
614        ):
615            config = self.get_config()
616            self.assertEqual(
617                config["DEFINES"],
618                {
619                    "FOO": "foo",
620                },
621            )
622            config = self.get_config(["--with-qux"])
623            self.assertEqual(
624                config["DEFINES"],
625                {
626                    "FOO": "foo",
627                    "QUX": "qux",
628                },
629            )
630
631    def test_imply_option_simple(self):
632        def get_config(*args):
633            return self.get_config(*args, configure="imply_option/simple.configure")
634
635        help, config = get_config(["--help"])
636        self.assertEqual(config, {})
637
638        config = get_config([])
639        self.assertEqual(config, {})
640
641        config = get_config(["--enable-foo"])
642        self.assertIn("BAR", config)
643        self.assertEqual(config["BAR"], PositiveOptionValue())
644
645        with self.assertRaises(InvalidOptionError) as e:
646            get_config(["--enable-foo", "--disable-bar"])
647
648        self.assertEqual(
649            str(e.exception),
650            "'--enable-bar' implied by '--enable-foo' conflicts with "
651            "'--disable-bar' from the command-line",
652        )
653
654    def test_imply_option_negative(self):
655        def get_config(*args):
656            return self.get_config(*args, configure="imply_option/negative.configure")
657
658        help, config = get_config(["--help"])
659        self.assertEqual(config, {})
660
661        config = get_config([])
662        self.assertEqual(config, {})
663
664        config = get_config(["--enable-foo"])
665        self.assertIn("BAR", config)
666        self.assertEqual(config["BAR"], NegativeOptionValue())
667
668        with self.assertRaises(InvalidOptionError) as e:
669            get_config(["--enable-foo", "--enable-bar"])
670
671        self.assertEqual(
672            str(e.exception),
673            "'--disable-bar' implied by '--enable-foo' conflicts with "
674            "'--enable-bar' from the command-line",
675        )
676
677        config = get_config(["--disable-hoge"])
678        self.assertIn("BAR", config)
679        self.assertEqual(config["BAR"], NegativeOptionValue())
680
681        with self.assertRaises(InvalidOptionError) as e:
682            get_config(["--disable-hoge", "--enable-bar"])
683
684        self.assertEqual(
685            str(e.exception),
686            "'--disable-bar' implied by '--disable-hoge' conflicts with "
687            "'--enable-bar' from the command-line",
688        )
689
690    def test_imply_option_values(self):
691        def get_config(*args):
692            return self.get_config(*args, configure="imply_option/values.configure")
693
694        help, config = get_config(["--help"])
695        self.assertEqual(config, {})
696
697        config = get_config([])
698        self.assertEqual(config, {})
699
700        config = get_config(["--enable-foo=a"])
701        self.assertIn("BAR", config)
702        self.assertEqual(config["BAR"], PositiveOptionValue(("a",)))
703
704        config = get_config(["--enable-foo=a,b"])
705        self.assertIn("BAR", config)
706        self.assertEqual(config["BAR"], PositiveOptionValue(("a", "b")))
707
708        with self.assertRaises(InvalidOptionError) as e:
709            get_config(["--enable-foo=a,b", "--disable-bar"])
710
711        self.assertEqual(
712            str(e.exception),
713            "'--enable-bar=a,b' implied by '--enable-foo' conflicts with "
714            "'--disable-bar' from the command-line",
715        )
716
717    def test_imply_option_infer(self):
718        def get_config(*args):
719            return self.get_config(*args, configure="imply_option/infer.configure")
720
721        help, config = get_config(["--help"])
722        self.assertEqual(config, {})
723
724        config = get_config([])
725        self.assertEqual(config, {})
726
727        with self.assertRaises(InvalidOptionError) as e:
728            get_config(["--enable-foo", "--disable-bar"])
729
730        self.assertEqual(
731            str(e.exception),
732            "'--enable-bar' implied by '--enable-foo' conflicts with "
733            "'--disable-bar' from the command-line",
734        )
735
736        with self.assertRaises(ConfigureError) as e:
737            self.get_config([], configure="imply_option/infer_ko.configure")
738
739        self.assertEqual(
740            str(e.exception),
741            "Cannot infer what implies '--enable-bar'. Please add a `reason` "
742            "to the `imply_option` call.",
743        )
744
745    def test_imply_option_immediate_value(self):
746        def get_config(*args):
747            return self.get_config(*args, configure="imply_option/imm.configure")
748
749        help, config = get_config(["--help"])
750        self.assertEqual(config, {})
751
752        config = get_config([])
753        self.assertEqual(config, {})
754
755        config_path = mozpath.abspath(
756            mozpath.join(test_data_path, "imply_option", "imm.configure")
757        )
758
759        with self.assertRaisesRegexp(
760            InvalidOptionError,
761            "--enable-foo' implied by 'imply_option at %s:7' conflicts "
762            "with '--disable-foo' from the command-line" % config_path,
763        ):
764            get_config(["--disable-foo"])
765
766        with self.assertRaisesRegexp(
767            InvalidOptionError,
768            "--enable-bar=foo,bar' implied by 'imply_option at %s:18' "
769            "conflicts with '--enable-bar=a,b,c' from the command-line" % config_path,
770        ):
771            get_config(["--enable-bar=a,b,c"])
772
773        with self.assertRaisesRegexp(
774            InvalidOptionError,
775            "--enable-baz=BAZ' implied by 'imply_option at %s:29' "
776            "conflicts with '--enable-baz=QUUX' from the command-line" % config_path,
777        ):
778            get_config(["--enable-baz=QUUX"])
779
780    def test_imply_option_failures(self):
781        with self.assertRaises(ConfigureError) as e:
782            with self.moz_configure(
783                """
784                imply_option('--with-foo', ('a',), 'bar')
785            """
786            ):
787                self.get_config()
788
789        self.assertEqual(
790            str(e.exception),
791            "`--with-foo`, emitted from `%s` line 2, is unknown."
792            % mozpath.join(test_data_path, "moz.configure"),
793        )
794
795        with self.assertRaises(TypeError) as e:
796            with self.moz_configure(
797                """
798                imply_option('--with-foo', 42, 'bar')
799
800                option('--with-foo', help='foo')
801                @depends('--with-foo')
802                def foo(value):
803                    return value
804            """
805            ):
806                self.get_config()
807
808        self.assertEqual(str(e.exception), "Unexpected type: 'int'")
809
810    def test_imply_option_when(self):
811        with self.moz_configure(
812            """
813            option('--with-foo', help='foo')
814            imply_option('--with-qux', True, when='--with-foo')
815            option('--with-qux', help='qux')
816            set_config('QUX', depends('--with-qux')(lambda x: x))
817        """
818        ):
819            config = self.get_config()
820            self.assertEqual(
821                config,
822                {
823                    "QUX": NegativeOptionValue(),
824                },
825            )
826
827            config = self.get_config(["--with-foo"])
828            self.assertEqual(
829                config,
830                {
831                    "QUX": PositiveOptionValue(),
832                },
833            )
834
835    def test_imply_option_dependency_loop(self):
836        with self.moz_configure(
837            """
838            option('--without-foo', help='foo')
839
840            @depends('--with-foo')
841            def qux_default(foo):
842                return bool(foo)
843
844            option('--with-qux', default=qux_default, help='qux')
845
846            imply_option('--with-foo', depends('--with-qux')(lambda x: x or None))
847
848            set_config('FOO', depends('--with-foo')(lambda x: x))
849            set_config('QUX', depends('--with-qux')(lambda x: x))
850        """
851        ):
852            config = self.get_config()
853            self.assertEqual(
854                config,
855                {
856                    "FOO": PositiveOptionValue(),
857                    "QUX": PositiveOptionValue(),
858                },
859            )
860
861            config = self.get_config(["--without-foo"])
862            self.assertEqual(
863                config,
864                {
865                    "FOO": NegativeOptionValue(),
866                    "QUX": NegativeOptionValue(),
867                },
868            )
869
870            config = self.get_config(["--with-qux"])
871            self.assertEqual(
872                config,
873                {
874                    "FOO": PositiveOptionValue(),
875                    "QUX": PositiveOptionValue(),
876                },
877            )
878
879            with self.assertRaises(InvalidOptionError) as e:
880                config = self.get_config(["--without-foo", "--with-qux"])
881
882            self.assertEqual(
883                str(e.exception),
884                "'--with-foo' implied by '--with-qux' conflicts "
885                "with '--without-foo' from the command-line",
886            )
887
888            config = self.get_config(["--without-qux"])
889            self.assertEqual(
890                config,
891                {
892                    "FOO": PositiveOptionValue(),
893                    "QUX": NegativeOptionValue(),
894                },
895            )
896
897        with self.moz_configure(
898            """
899            option('--with-foo', help='foo')
900
901            @depends('--with-foo')
902            def qux_default(foo):
903                return bool(foo)
904
905            option('--with-qux', default=qux_default, help='qux')
906
907            imply_option('--with-foo', depends('--with-qux')(lambda x: x or None))
908
909            set_config('FOO', depends('--with-foo')(lambda x: x))
910            set_config('QUX', depends('--with-qux')(lambda x: x))
911        """
912        ):
913            config = self.get_config()
914            self.assertEqual(
915                config,
916                {
917                    "FOO": NegativeOptionValue(),
918                    "QUX": NegativeOptionValue(),
919                },
920            )
921
922            config = self.get_config(["--with-foo"])
923            self.assertEqual(
924                config,
925                {
926                    "FOO": PositiveOptionValue(),
927                    "QUX": PositiveOptionValue(),
928                },
929            )
930
931            with self.assertRaises(InvalidOptionError) as e:
932                config = self.get_config(["--with-qux"])
933
934            self.assertEqual(
935                str(e.exception),
936                "'--with-foo' implied by '--with-qux' conflicts "
937                "with '--without-foo' from the default",
938            )
939
940            with self.assertRaises(InvalidOptionError) as e:
941                config = self.get_config(["--without-foo", "--with-qux"])
942
943            self.assertEqual(
944                str(e.exception),
945                "'--with-foo' implied by '--with-qux' conflicts "
946                "with '--without-foo' from the command-line",
947            )
948
949            config = self.get_config(["--without-qux"])
950            self.assertEqual(
951                config,
952                {
953                    "FOO": NegativeOptionValue(),
954                    "QUX": NegativeOptionValue(),
955                },
956            )
957
958        config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure"))
959
960        # Same test as above, but using `when` in the `imply_option`.
961        with self.moz_configure(
962            """
963            option('--with-foo', help='foo')
964
965            @depends('--with-foo')
966            def qux_default(foo):
967                return bool(foo)
968
969            option('--with-qux', default=qux_default, help='qux')
970
971            imply_option('--with-foo', True, when='--with-qux')
972
973            set_config('FOO', depends('--with-foo')(lambda x: x))
974            set_config('QUX', depends('--with-qux')(lambda x: x))
975        """
976        ):
977            config = self.get_config()
978            self.assertEqual(
979                config,
980                {
981                    "FOO": NegativeOptionValue(),
982                    "QUX": NegativeOptionValue(),
983                },
984            )
985
986            config = self.get_config(["--with-foo"])
987            self.assertEqual(
988                config,
989                {
990                    "FOO": PositiveOptionValue(),
991                    "QUX": PositiveOptionValue(),
992                },
993            )
994
995            with self.assertRaises(InvalidOptionError) as e:
996                config = self.get_config(["--with-qux"])
997
998            self.assertEqual(
999                str(e.exception),
1000                "'--with-foo' implied by 'imply_option at %s:10' conflicts "
1001                "with '--without-foo' from the default" % config_path,
1002            )
1003
1004            with self.assertRaises(InvalidOptionError) as e:
1005                config = self.get_config(["--without-foo", "--with-qux"])
1006
1007            self.assertEqual(
1008                str(e.exception),
1009                "'--with-foo' implied by 'imply_option at %s:10' conflicts "
1010                "with '--without-foo' from the command-line" % config_path,
1011            )
1012
1013            config = self.get_config(["--without-qux"])
1014            self.assertEqual(
1015                config,
1016                {
1017                    "FOO": NegativeOptionValue(),
1018                    "QUX": NegativeOptionValue(),
1019                },
1020            )
1021
1022    def test_imply_option_recursion(self):
1023        config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure"))
1024
1025        message = (
1026            "'--without-foo' appears somewhere in the direct or indirect dependencies "
1027            "when resolving imply_option at %s:8" % config_path
1028        )
1029
1030        with self.moz_configure(
1031            """
1032            option('--without-foo', help='foo')
1033
1034            imply_option('--with-qux', depends('--with-foo')(lambda x: x or None))
1035
1036            option('--with-qux', help='qux')
1037
1038            imply_option('--with-foo', depends('--with-qux')(lambda x: x or None))
1039
1040            set_config('FOO', depends('--with-foo')(lambda x: x))
1041            set_config('QUX', depends('--with-qux')(lambda x: x))
1042        """
1043        ):
1044            # Note: no error is detected when the depends function in the
1045            # imply_options resolve to None, which disables the imply_option.
1046
1047            with self.assertRaises(ConfigureError) as e:
1048                self.get_config()
1049
1050            self.assertEqual(str(e.exception), message)
1051
1052            with self.assertRaises(ConfigureError) as e:
1053                self.get_config(["--with-qux"])
1054
1055            self.assertEqual(str(e.exception), message)
1056
1057            with self.assertRaises(ConfigureError) as e:
1058                self.get_config(["--without-foo", "--with-qux"])
1059
1060            self.assertEqual(str(e.exception), message)
1061
1062    def test_option_failures(self):
1063        with self.assertRaises(ConfigureError) as e:
1064            with self.moz_configure('option("--with-foo", help="foo")'):
1065                self.get_config()
1066
1067        self.assertEqual(
1068            str(e.exception),
1069            "Option `--with-foo` is not handled ; reference it with a @depends",
1070        )
1071
1072        with self.assertRaises(ConfigureError) as e:
1073            with self.moz_configure(
1074                """
1075                option("--with-foo", help="foo")
1076                option("--with-foo", help="foo")
1077            """
1078            ):
1079                self.get_config()
1080
1081        self.assertEqual(str(e.exception), "Option `--with-foo` already defined")
1082
1083        with self.assertRaises(ConfigureError) as e:
1084            with self.moz_configure(
1085                """
1086                option(env="MOZ_FOO", help="foo")
1087                option(env="MOZ_FOO", help="foo")
1088            """
1089            ):
1090                self.get_config()
1091
1092        self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined")
1093
1094        with self.assertRaises(ConfigureError) as e:
1095            with self.moz_configure(
1096                """
1097                option('--with-foo', env="MOZ_FOO", help="foo")
1098                option(env="MOZ_FOO", help="foo")
1099            """
1100            ):
1101                self.get_config()
1102
1103        self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined")
1104
1105        with self.assertRaises(ConfigureError) as e:
1106            with self.moz_configure(
1107                """
1108                option(env="MOZ_FOO", help="foo")
1109                option('--with-foo', env="MOZ_FOO", help="foo")
1110            """
1111            ):
1112                self.get_config()
1113
1114        self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined")
1115
1116        with self.assertRaises(ConfigureError) as e:
1117            with self.moz_configure(
1118                """
1119                option('--with-foo', env="MOZ_FOO", help="foo")
1120                option('--with-foo', help="foo")
1121            """
1122            ):
1123                self.get_config()
1124
1125        self.assertEqual(str(e.exception), "Option `--with-foo` already defined")
1126
1127    def test_option_when(self):
1128        with self.moz_configure(
1129            """
1130            option('--with-foo', help='foo', when=True)
1131            option('--with-bar', help='bar', when=False)
1132            option('--with-qux', env="QUX", help='qux', when='--with-foo')
1133
1134            set_config('FOO', depends('--with-foo', when=True)(lambda x: x))
1135            set_config('BAR', depends('--with-bar', when=False)(lambda x: x))
1136            set_config('QUX', depends('--with-qux', when='--with-foo')(lambda x: x))
1137        """
1138        ):
1139            config = self.get_config()
1140            self.assertEqual(
1141                config,
1142                {
1143                    "FOO": NegativeOptionValue(),
1144                },
1145            )
1146
1147            config = self.get_config(["--with-foo"])
1148            self.assertEqual(
1149                config,
1150                {
1151                    "FOO": PositiveOptionValue(),
1152                    "QUX": NegativeOptionValue(),
1153                },
1154            )
1155
1156            config = self.get_config(["--with-foo", "--with-qux"])
1157            self.assertEqual(
1158                config,
1159                {
1160                    "FOO": PositiveOptionValue(),
1161                    "QUX": PositiveOptionValue(),
1162                },
1163            )
1164
1165            with self.assertRaises(InvalidOptionError) as e:
1166                self.get_config(["--with-bar"])
1167
1168            self.assertEqual(
1169                str(e.exception), "--with-bar is not available in this configuration"
1170            )
1171
1172            with self.assertRaises(InvalidOptionError) as e:
1173                self.get_config(["--with-qux"])
1174
1175            self.assertEqual(
1176                str(e.exception), "--with-qux is not available in this configuration"
1177            )
1178
1179            with self.assertRaises(InvalidOptionError) as e:
1180                self.get_config(["QUX=1"])
1181
1182            self.assertEqual(
1183                str(e.exception), "QUX is not available in this configuration"
1184            )
1185
1186            config = self.get_config(env={"QUX": "1"})
1187            self.assertEqual(
1188                config,
1189                {
1190                    "FOO": NegativeOptionValue(),
1191                },
1192            )
1193
1194            help, config = self.get_config(["--help"])
1195            self.assertEqual(
1196                help.replace("\\", "/"),
1197                textwrap.dedent(
1198                    """\
1199                Usage: configure [options]
1200
1201                Options: [defaults in brackets after descriptions]
1202                  Help options:
1203                    --help                    print this message
1204
1205                  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:
1206                    --with-foo                foo
1207
1208
1209                Environment variables:
1210            """
1211                ),
1212            )
1213
1214            help, config = self.get_config(["--help", "--with-foo"])
1215            self.assertEqual(
1216                help.replace("\\", "/"),
1217                textwrap.dedent(
1218                    """\
1219                Usage: configure [options]
1220
1221                Options: [defaults in brackets after descriptions]
1222                  Help options:
1223                    --help                    print this message
1224
1225                  Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:
1226                    --with-foo                foo
1227                    --with-qux                qux
1228
1229
1230                Environment variables:
1231            """
1232                ),
1233            )
1234
1235        with self.moz_configure(
1236            """
1237            option('--with-foo', help='foo', when=True)
1238            set_config('FOO', depends('--with-foo')(lambda x: x))
1239        """
1240        ):
1241            with self.assertRaises(ConfigureError) as e:
1242                self.get_config()
1243
1244            self.assertEqual(
1245                str(e.exception),
1246                "@depends function needs the same `when` as " "options it depends on",
1247            )
1248
1249        with self.moz_configure(
1250            """
1251            @depends(when=True)
1252            def always():
1253                return True
1254            @depends(when=True)
1255            def always2():
1256                return True
1257            option('--with-foo', help='foo', when=always)
1258            set_config('FOO', depends('--with-foo', when=always2)(lambda x: x))
1259        """
1260        ):
1261            with self.assertRaises(ConfigureError) as e:
1262                self.get_config()
1263
1264            self.assertEqual(
1265                str(e.exception),
1266                "@depends function needs the same `when` as " "options it depends on",
1267            )
1268
1269        with self.moz_configure(
1270            """
1271            @depends(when=True)
1272            def always():
1273                return True
1274            @depends(when=True)
1275            def always2():
1276                return True
1277            with only_when(always2):
1278                option('--with-foo', help='foo', when=always)
1279                # include() triggers resolution of its dependencies, and their
1280                # side effects.
1281                include(depends('--with-foo', when=always)(lambda x: x))
1282                # The sandbox should figure that the `when` here is
1283                # appropriate. Bad behavior in CombinedDependsFunction.__eq__
1284                # made this fail in the past.
1285                set_config('FOO', depends('--with-foo', when=always)(lambda x: x))
1286        """
1287        ):
1288            self.get_config()
1289
1290        with self.moz_configure(
1291            """
1292            option('--with-foo', help='foo')
1293            option('--without-bar', help='bar', when='--with-foo')
1294            option('--with-qux', help='qux', when='--with-bar')
1295            set_config('QUX', True, when='--with-qux')
1296        """
1297        ):
1298            # These are valid:
1299            self.get_config(["--with-foo"])
1300            self.get_config(["--with-foo", "--with-bar"])
1301            self.get_config(["--with-foo", "--without-bar"])
1302            self.get_config(["--with-foo", "--with-bar", "--with-qux"])
1303            self.get_config(["--with-foo", "--with-bar", "--without-qux"])
1304            with self.assertRaises(InvalidOptionError) as e:
1305                self.get_config(["--with-bar"])
1306            with self.assertRaises(InvalidOptionError) as e:
1307                self.get_config(["--without-bar"])
1308            with self.assertRaises(InvalidOptionError) as e:
1309                self.get_config(["--with-qux"])
1310            with self.assertRaises(InvalidOptionError) as e:
1311                self.get_config(["--without-qux"])
1312            with self.assertRaises(InvalidOptionError) as e:
1313                self.get_config(["--with-foo", "--without-bar", "--with-qux"])
1314            with self.assertRaises(InvalidOptionError) as e:
1315                self.get_config(["--with-foo", "--without-bar", "--without-qux"])
1316
1317    def test_include_failures(self):
1318        with self.assertRaises(ConfigureError) as e:
1319            with self.moz_configure('include("../foo.configure")'):
1320                self.get_config()
1321
1322        self.assertEqual(
1323            str(e.exception),
1324            "Cannot include `%s` because it is not in a subdirectory of `%s`"
1325            % (
1326                mozpath.normpath(mozpath.join(test_data_path, "..", "foo.configure")),
1327                mozpath.normsep(test_data_path),
1328            ),
1329        )
1330
1331        with self.assertRaises(ConfigureError) as e:
1332            with self.moz_configure(
1333                """
1334                include('extra.configure')
1335                include('extra.configure')
1336            """
1337            ):
1338                self.get_config()
1339
1340        self.assertEqual(
1341            str(e.exception),
1342            "Cannot include `%s` because it was included already."
1343            % mozpath.normpath(mozpath.join(test_data_path, "extra.configure")),
1344        )
1345
1346        with self.assertRaises(TypeError) as e:
1347            with self.moz_configure(
1348                """
1349                include(42)
1350            """
1351            ):
1352                self.get_config()
1353
1354        self.assertEqual(str(e.exception), "Unexpected type: 'int'")
1355
1356    def test_include_when(self):
1357        with MockedOpen(
1358            {
1359                os.path.join(test_data_path, "moz.configure"): textwrap.dedent(
1360                    """
1361                option('--with-foo', help='foo')
1362
1363                include('always.configure', when=True)
1364                include('never.configure', when=False)
1365                include('foo.configure', when='--with-foo')
1366
1367                set_config('FOO', foo)
1368                set_config('BAR', bar)
1369                set_config('QUX', qux)
1370            """
1371                ),
1372                os.path.join(test_data_path, "always.configure"): textwrap.dedent(
1373                    """
1374                option('--with-bar', help='bar')
1375                @depends('--with-bar')
1376                def bar(x):
1377                    if x:
1378                        return 'bar'
1379            """
1380                ),
1381                os.path.join(test_data_path, "never.configure"): textwrap.dedent(
1382                    """
1383                option('--with-qux', help='qux')
1384                @depends('--with-qux')
1385                def qux(x):
1386                    if x:
1387                        return 'qux'
1388            """
1389                ),
1390                os.path.join(test_data_path, "foo.configure"): textwrap.dedent(
1391                    """
1392                option('--with-foo-really', help='really foo')
1393                @depends('--with-foo-really')
1394                def foo(x):
1395                    if x:
1396                        return 'foo'
1397
1398                include('foo2.configure', when='--with-foo-really')
1399            """
1400                ),
1401                os.path.join(test_data_path, "foo2.configure"): textwrap.dedent(
1402                    """
1403                set_config('FOO2', True)
1404            """
1405                ),
1406            }
1407        ):
1408            config = self.get_config()
1409            self.assertEqual(config, {})
1410
1411            config = self.get_config(["--with-foo"])
1412            self.assertEqual(config, {})
1413
1414            config = self.get_config(["--with-bar"])
1415            self.assertEqual(
1416                config,
1417                {
1418                    "BAR": "bar",
1419                },
1420            )
1421
1422            with self.assertRaises(InvalidOptionError) as e:
1423                self.get_config(["--with-qux"])
1424
1425            self.assertEqual(
1426                str(e.exception), "--with-qux is not available in this configuration"
1427            )
1428
1429            config = self.get_config(["--with-foo", "--with-foo-really"])
1430            self.assertEqual(
1431                config,
1432                {
1433                    "FOO": "foo",
1434                    "FOO2": True,
1435                },
1436            )
1437
1438    def test_sandbox_failures(self):
1439        with self.assertRaises(KeyError) as e:
1440            with self.moz_configure(
1441                """
1442                include = 42
1443            """
1444            ):
1445                self.get_config()
1446
1447        self.assertIn("Cannot reassign builtins", str(e.exception))
1448
1449        with self.assertRaises(KeyError) as e:
1450            with self.moz_configure(
1451                """
1452                foo = 42
1453            """
1454            ):
1455                self.get_config()
1456
1457        self.assertIn(
1458            "Cannot assign `foo` because it is neither a @depends nor a " "@template",
1459            str(e.exception),
1460        )
1461
1462    def test_depends_failures(self):
1463        with self.assertRaises(ConfigureError) as e:
1464            with self.moz_configure(
1465                """
1466                @depends()
1467                def foo():
1468                    return
1469            """
1470            ):
1471                self.get_config()
1472
1473        self.assertEqual(str(e.exception), "@depends needs at least one argument")
1474
1475        with self.assertRaises(ConfigureError) as e:
1476            with self.moz_configure(
1477                """
1478                @depends('--with-foo')
1479                def foo(value):
1480                    return value
1481            """
1482            ):
1483                self.get_config()
1484
1485        self.assertEqual(
1486            str(e.exception),
1487            "'--with-foo' is not a known option. Maybe it's " "declared too late?",
1488        )
1489
1490        with self.assertRaises(ConfigureError) as e:
1491            with self.moz_configure(
1492                """
1493                @depends('--with-foo=42')
1494                def foo(value):
1495                    return value
1496            """
1497            ):
1498                self.get_config()
1499
1500        self.assertEqual(str(e.exception), "Option must not contain an '='")
1501
1502        with self.assertRaises(TypeError) as e:
1503            with self.moz_configure(
1504                """
1505                @depends(42)
1506                def foo(value):
1507                    return value
1508            """
1509            ):
1510                self.get_config()
1511
1512        self.assertEqual(
1513            str(e.exception),
1514            "Cannot use object of type 'int' as argument " "to @depends",
1515        )
1516
1517        with self.assertRaises(ConfigureError) as e:
1518            with self.moz_configure(
1519                """
1520                @depends('--help')
1521                def foo(value):
1522                    yield
1523            """
1524            ):
1525                self.get_config()
1526
1527        self.assertEqual(
1528            str(e.exception), "Cannot decorate generator functions with @depends"
1529        )
1530
1531        with self.assertRaises(TypeError) as e:
1532            with self.moz_configure(
1533                """
1534                depends('--help')(42)
1535            """
1536            ):
1537                self.get_config()
1538
1539        self.assertEqual(str(e.exception), "Unexpected type: 'int'")
1540
1541        with self.assertRaises(ConfigureError) as e:
1542            with self.moz_configure(
1543                """
1544                option('--foo', help='foo')
1545                @depends('--foo')
1546                def foo(value):
1547                    return value
1548
1549                foo()
1550            """
1551            ):
1552                self.get_config()
1553
1554        self.assertEqual(str(e.exception), "The `foo` function may not be called")
1555
1556        with self.assertRaises(TypeError) as e:
1557            with self.moz_configure(
1558                """
1559                @depends('--help', foo=42)
1560                def foo(_):
1561                    return
1562            """
1563            ):
1564                self.get_config()
1565
1566        self.assertEqual(
1567            str(e.exception), "depends_impl() got an unexpected keyword argument 'foo'"
1568        )
1569
1570    def test_depends_when(self):
1571        with self.moz_configure(
1572            """
1573            @depends(when=True)
1574            def foo():
1575                return 'foo'
1576
1577            set_config('FOO', foo)
1578
1579            @depends(when=False)
1580            def bar():
1581                return 'bar'
1582
1583            set_config('BAR', bar)
1584
1585            option('--with-qux', help='qux')
1586            @depends(when='--with-qux')
1587            def qux():
1588                return 'qux'
1589
1590            set_config('QUX', qux)
1591        """
1592        ):
1593            config = self.get_config()
1594            self.assertEqual(
1595                config,
1596                {
1597                    "FOO": "foo",
1598                },
1599            )
1600
1601            config = self.get_config(["--with-qux"])
1602            self.assertEqual(
1603                config,
1604                {
1605                    "FOO": "foo",
1606                    "QUX": "qux",
1607                },
1608            )
1609
1610    def test_imports_failures(self):
1611        with self.assertRaises(ConfigureError) as e:
1612            with self.moz_configure(
1613                """
1614                @imports('os')
1615                @template
1616                def foo(value):
1617                    return value
1618            """
1619            ):
1620                self.get_config()
1621
1622        self.assertEqual(str(e.exception), "@imports must appear after @template")
1623
1624        with self.assertRaises(ConfigureError) as e:
1625            with self.moz_configure(
1626                """
1627                option('--foo', help='foo')
1628                @imports('os')
1629                @depends('--foo')
1630                def foo(value):
1631                    return value
1632            """
1633            ):
1634                self.get_config()
1635
1636        self.assertEqual(str(e.exception), "@imports must appear after @depends")
1637
1638        for import_ in (
1639            "42",
1640            "_from=42, _import='os'",
1641            "_from='os', _import='path', _as=42",
1642        ):
1643            with self.assertRaises(TypeError) as e:
1644                with self.moz_configure(
1645                    """
1646                    @imports(%s)
1647                    @template
1648                    def foo(value):
1649                        return value
1650                """
1651                    % import_
1652                ):
1653                    self.get_config()
1654
1655            self.assertEqual(str(e.exception), "Unexpected type: 'int'")
1656
1657        with self.assertRaises(TypeError) as e:
1658            with self.moz_configure(
1659                """
1660                @imports('os', 42)
1661                @template
1662                def foo(value):
1663                    return value
1664            """
1665            ):
1666                self.get_config()
1667
1668        self.assertEqual(str(e.exception), "Unexpected type: 'int'")
1669
1670        with self.assertRaises(ValueError) as e:
1671            with self.moz_configure(
1672                """
1673                @imports('os*')
1674                def foo(value):
1675                    return value
1676            """
1677            ):
1678                self.get_config()
1679
1680        self.assertEqual(str(e.exception), "Invalid argument to @imports: 'os*'")
1681
1682    def test_only_when(self):
1683        moz_configure = """
1684            option('--enable-when', help='when')
1685            @depends('--enable-when', '--help')
1686            def when(value, _):
1687                return bool(value)
1688
1689            with only_when(when):
1690                option('--foo', nargs='*', help='foo')
1691                @depends('--foo')
1692                def foo(value):
1693                    return value
1694
1695                set_config('FOO', foo)
1696                set_define('FOO', foo)
1697
1698            # It is possible to depend on a function defined in a only_when
1699            # block. It then resolves to `None`.
1700            set_config('BAR', depends(foo)(lambda x: x))
1701            set_define('BAR', depends(foo)(lambda x: x))
1702        """
1703
1704        with self.moz_configure(moz_configure):
1705            config = self.get_config()
1706            self.assertEqual(
1707                config,
1708                {
1709                    "DEFINES": {},
1710                },
1711            )
1712
1713            config = self.get_config(["--enable-when"])
1714            self.assertEqual(
1715                config,
1716                {
1717                    "BAR": NegativeOptionValue(),
1718                    "FOO": NegativeOptionValue(),
1719                    "DEFINES": {
1720                        "BAR": NegativeOptionValue(),
1721                        "FOO": NegativeOptionValue(),
1722                    },
1723                },
1724            )
1725
1726            config = self.get_config(["--enable-when", "--foo=bar"])
1727            self.assertEqual(
1728                config,
1729                {
1730                    "BAR": PositiveOptionValue(["bar"]),
1731                    "FOO": PositiveOptionValue(["bar"]),
1732                    "DEFINES": {
1733                        "BAR": PositiveOptionValue(["bar"]),
1734                        "FOO": PositiveOptionValue(["bar"]),
1735                    },
1736                },
1737            )
1738
1739            # The --foo option doesn't exist when --enable-when is not given.
1740            with self.assertRaises(InvalidOptionError) as e:
1741                self.get_config(["--foo"])
1742
1743            self.assertEqual(
1744                str(e.exception), "--foo is not available in this configuration"
1745            )
1746
1747        # Cannot depend on an option defined in a only_when block, because we
1748        # don't know what OptionValue would make sense.
1749        with self.moz_configure(
1750            moz_configure
1751            + """
1752            set_config('QUX', depends('--foo')(lambda x: x))
1753        """
1754        ):
1755            with self.assertRaises(ConfigureError) as e:
1756                self.get_config()
1757
1758            self.assertEqual(
1759                str(e.exception),
1760                "@depends function needs the same `when` as " "options it depends on",
1761            )
1762
1763        with self.moz_configure(
1764            moz_configure
1765            + """
1766            set_config('QUX', depends('--foo', when=when)(lambda x: x))
1767        """
1768        ):
1769            self.get_config(["--enable-when"])
1770
1771        # Using imply_option for an option defined in a only_when block fails
1772        # similarly if the imply_option happens outside the block.
1773        with self.moz_configure(
1774            """
1775            imply_option('--foo', True)
1776        """
1777            + moz_configure
1778        ):
1779            with self.assertRaises(InvalidOptionError) as e:
1780                self.get_config()
1781
1782            self.assertEqual(
1783                str(e.exception), "--foo is not available in this configuration"
1784            )
1785
1786        # And similarly doesn't fail when the condition is true.
1787        with self.moz_configure(
1788            """
1789            imply_option('--foo', True)
1790        """
1791            + moz_configure
1792        ):
1793            self.get_config(["--enable-when"])
1794
1795    def test_depends_binary_ops(self):
1796        with self.moz_configure(
1797            """
1798            option('--foo', nargs=1, help='foo')
1799            @depends('--foo')
1800            def foo(value):
1801                return value or 0
1802
1803            option('--bar', nargs=1, help='bar')
1804            @depends('--bar')
1805            def bar(value):
1806                return value or ''
1807
1808            option('--baz', nargs=1, help='baz')
1809            @depends('--baz')
1810            def baz(value):
1811                return value
1812
1813            set_config('FOOorBAR', foo | bar)
1814            set_config('FOOorBARorBAZ', foo | bar | baz)
1815            set_config('FOOandBAR', foo & bar)
1816            set_config('FOOandBARandBAZ', foo & bar & baz)
1817        """
1818        ):
1819            for foo_opt, foo_value in (
1820                ("", 0),
1821                ("--foo=foo", PositiveOptionValue(("foo",))),
1822            ):
1823                for bar_opt, bar_value in (
1824                    ("", ""),
1825                    ("--bar=bar", PositiveOptionValue(("bar",))),
1826                ):
1827                    for baz_opt, baz_value in (
1828                        ("", NegativeOptionValue()),
1829                        ("--baz=baz", PositiveOptionValue(("baz",))),
1830                    ):
1831                        config = self.get_config(
1832                            [x for x in (foo_opt, bar_opt, baz_opt) if x]
1833                        )
1834                        self.assertEqual(
1835                            config,
1836                            {
1837                                "FOOorBAR": foo_value or bar_value,
1838                                "FOOorBARorBAZ": foo_value or bar_value or baz_value,
1839                                "FOOandBAR": foo_value and bar_value,
1840                                "FOOandBARandBAZ": foo_value
1841                                and bar_value
1842                                and baz_value,
1843                            },
1844                        )
1845
1846    def test_depends_getattr(self):
1847        with self.moz_configure(
1848            """
1849            @imports(_from='mozbuild.util', _import='ReadOnlyNamespace')
1850            def namespace(**kwargs):
1851                return ReadOnlyNamespace(**kwargs)
1852
1853            option('--foo', nargs=1, help='foo')
1854            @depends('--foo')
1855            def foo(value):
1856                return value
1857
1858            option('--bar', nargs=1, help='bar')
1859            @depends('--bar')
1860            def bar(value):
1861                return value or None
1862
1863            @depends(foo, bar)
1864            def foobar(foo, bar):
1865                return namespace(foo=foo, bar=bar)
1866
1867            set_config('FOO', foobar.foo)
1868            set_config('BAR', foobar.bar)
1869            set_config('BAZ', foobar.baz)
1870        """
1871        ):
1872            config = self.get_config()
1873            self.assertEqual(
1874                config,
1875                {
1876                    "FOO": NegativeOptionValue(),
1877                },
1878            )
1879
1880            config = self.get_config(["--foo=foo"])
1881            self.assertEqual(
1882                config,
1883                {
1884                    "FOO": PositiveOptionValue(("foo",)),
1885                },
1886            )
1887
1888            config = self.get_config(["--bar=bar"])
1889            self.assertEqual(
1890                config,
1891                {
1892                    "FOO": NegativeOptionValue(),
1893                    "BAR": PositiveOptionValue(("bar",)),
1894                },
1895            )
1896
1897            config = self.get_config(["--foo=foo", "--bar=bar"])
1898            self.assertEqual(
1899                config,
1900                {
1901                    "FOO": PositiveOptionValue(("foo",)),
1902                    "BAR": PositiveOptionValue(("bar",)),
1903                },
1904            )
1905
1906
1907if __name__ == "__main__":
1908    main()
1909