diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /python/mozbuild/mozbuild/test/configure | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'python/mozbuild/mozbuild/test/configure')
28 files changed, 9545 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/test/configure/common.py b/python/mozbuild/mozbuild/test/configure/common.py new file mode 100644 index 0000000000..7dc1b85b22 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/common.py @@ -0,0 +1,307 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import copy +import errno +import os +import subprocess +import sys +import tempfile +import unittest + +import six +from buildconfig import topobjdir, topsrcdir +from mozpack import path as mozpath +from six import StringIO, string_types + +from mozbuild.configure import ConfigureSandbox +from mozbuild.util import ReadOnlyNamespace, memoized_property + + +def fake_short_path(path): + if sys.platform.startswith("win"): + return "/".join( + p.split(" ", 1)[0] + "~1" if " " in p else p for p in mozpath.split(path) + ) + return path + + +def ensure_exe_extension(path): + if sys.platform.startswith("win"): + return path + ".exe" + return path + + +class ConfigureTestVFS(object): + def __init__(self, paths): + self._paths = set(mozpath.abspath(p) for p in paths) + + def _real_file(self, path): + return mozpath.basedir(path, [topsrcdir, topobjdir, tempfile.gettempdir()]) + + def exists(self, path): + if path in self._paths: + return True + if self._real_file(path): + return os.path.exists(path) + return False + + def isfile(self, path): + path = mozpath.abspath(path) + if path in self._paths: + return True + if self._real_file(path): + return os.path.isfile(path) + return False + + def expanduser(self, path): + return os.path.expanduser(path) + + def isdir(self, path): + path = mozpath.abspath(path) + if any(mozpath.basedir(mozpath.dirname(p), [path]) for p in self._paths): + return True + if self._real_file(path): + return os.path.isdir(path) + return False + + def getsize(self, path): + if not self._real_file(path): + raise FileNotFoundError(path) + return os.path.getsize(path) + + +class ConfigureTestSandbox(ConfigureSandbox): + """Wrapper around the ConfigureSandbox for testing purposes. + + Its arguments are the same as ConfigureSandbox, except for the additional + `paths` argument, which is a dict where the keys are file paths and the + values are either None or a function that will be called when the sandbox + calls an implemented function from subprocess with the key as command. + When the command is CONFIG_SHELL, the function for the path of the script + that follows will be called. + + The API for those functions is: + retcode, stdout, stderr = func(stdin, args) + + This class is only meant to implement the minimal things to make + moz.configure testing possible. As such, it takes shortcuts. + """ + + def __init__(self, paths, config, environ, *args, **kwargs): + self._search_path = environ.get("PATH", "").split(os.pathsep) + + self._subprocess_paths = { + mozpath.abspath(k): v for k, v in six.iteritems(paths) if v + } + + paths = list(paths) + + environ = copy.copy(environ) + if "CONFIG_SHELL" not in environ: + environ["CONFIG_SHELL"] = mozpath.abspath("/bin/sh") + self._subprocess_paths[environ["CONFIG_SHELL"]] = self.shell + paths.append(environ["CONFIG_SHELL"]) + self._subprocess_paths[ + mozpath.join(topsrcdir, "build/win32/vswhere.exe") + ] = self.vswhere + + vfs = ConfigureTestVFS(paths) + + os_path = {k: getattr(vfs, k) for k in dir(vfs) if not k.startswith("_")} + + os_path.update(self.OS.path.__dict__) + + os_contents = {} + exec("from os import *", {}, os_contents) + os_contents["path"] = ReadOnlyNamespace(**os_path) + os_contents["environ"] = dict(environ) + self.imported_os = ReadOnlyNamespace(**os_contents) + + super(ConfigureTestSandbox, self).__init__(config, environ, *args, **kwargs) + + @memoized_property + def _wrapped_mozfile(self): + return ReadOnlyNamespace(which=self.which) + + @memoized_property + def _wrapped_os(self): + return self.imported_os + + @memoized_property + def _wrapped_subprocess(self): + return ReadOnlyNamespace( + CalledProcessError=subprocess.CalledProcessError, + check_output=self.check_output, + PIPE=subprocess.PIPE, + STDOUT=subprocess.STDOUT, + Popen=self.Popen, + ) + + @memoized_property + def _wrapped_ctypes(self): + class CTypesFunc(object): + def __init__(self, func): + self._func = func + + def __call__(self, *args, **kwargs): + return self._func(*args, **kwargs) + + return ReadOnlyNamespace( + create_unicode_buffer=self.create_unicode_buffer, + windll=ReadOnlyNamespace( + kernel32=ReadOnlyNamespace( + GetShortPathNameW=CTypesFunc(self.GetShortPathNameW) + ) + ), + wintypes=ReadOnlyNamespace(LPCWSTR=0, LPWSTR=1, DWORD=2), + ) + + @memoized_property + def _wrapped__winreg(self): + def OpenKey(*args, **kwargs): + raise WindowsError() + + return ReadOnlyNamespace(HKEY_LOCAL_MACHINE=0, OpenKey=OpenKey) + + def create_unicode_buffer(self, *args, **kwargs): + class Buffer(object): + def __init__(self): + self.value = "" + + return Buffer() + + def GetShortPathNameW(self, path_in, path_out, length): + path_out.value = fake_short_path(path_in) + return length + + def which(self, command, mode=None, path=None, exts=None): + if isinstance(path, string_types): + path = path.split(os.pathsep) + + for parent in path or self._search_path: + c = mozpath.abspath(mozpath.join(parent, command)) + for candidate in (c, ensure_exe_extension(c)): + if self.imported_os.path.exists(candidate): + return candidate + return None + + def Popen(self, args, stdin=None, stdout=None, stderr=None, **kargs): + program = self.which(args[0]) + if not program: + raise OSError(errno.ENOENT, "File not found") + + func = self._subprocess_paths.get(program) + retcode, stdout, stderr = func(stdin, args[1:]) + + class Process(object): + def communicate(self, stdin=None): + return stdout, stderr + + def wait(self): + return retcode + + return Process() + + def check_output(self, args, **kwargs): + proc = self.Popen(args, **kwargs) + stdout, stderr = proc.communicate() + retcode = proc.wait() + if retcode: + raise subprocess.CalledProcessError(retcode, args, stdout) + return stdout + + def shell(self, stdin, args): + script = mozpath.abspath(args[0]) + if script in self._subprocess_paths: + return self._subprocess_paths[script](stdin, args[1:]) + return 127, "", "File not found" + + def vswhere(self, stdin, args): + return 0, "[]", "" + + def get_config(self, name): + # Like the loop in ConfigureSandbox.run, but only execute the code + # associated with the given config item. + for func, args in self._execution_queue: + if ( + func == self._resolve_and_set + and args[0] is self._config + and args[1] == name + ): + func(*args) + return self._config.get(name) + + +class BaseConfigureTest(unittest.TestCase): + HOST = "x86_64-pc-linux-gnu" + + def setUp(self): + self._cwd = os.getcwd() + os.chdir(topobjdir) + + def tearDown(self): + os.chdir(self._cwd) + + def config_guess(self, stdin, args): + return 0, self.HOST, "" + + def config_sub(self, stdin, args): + return 0, args[0], "" + + def get_sandbox( + self, + paths, + config, + args=[], + environ={}, + mozconfig="", + out=None, + logger=None, + cls=ConfigureTestSandbox, + ): + kwargs = {} + if logger: + kwargs["logger"] = logger + else: + if not out: + out = StringIO() + kwargs["stdout"] = out + kwargs["stderr"] = out + + if hasattr(self, "TARGET"): + target = ["--target=%s" % self.TARGET] + else: + target = [] + + if mozconfig: + fh, mozconfig_path = tempfile.mkstemp(text=True) + os.write(fh, six.ensure_binary(mozconfig)) + os.close(fh) + else: + mozconfig_path = os.path.join( + os.path.dirname(__file__), "data", "empty_mozconfig" + ) + + try: + environ = dict( + environ, + OLD_CONFIGURE=os.path.join(topsrcdir, "old-configure"), + MOZCONFIG=mozconfig_path, + ) + + paths = dict(paths) + autoconf_dir = mozpath.join(topsrcdir, "build", "autoconf") + paths[mozpath.join(autoconf_dir, "config.guess")] = self.config_guess + paths[mozpath.join(autoconf_dir, "config.sub")] = self.config_sub + + sandbox = cls( + paths, config, environ, ["configure"] + target + args, **kwargs + ) + sandbox.include_file(os.path.join(topsrcdir, "moz.configure")) + + return sandbox + finally: + if mozconfig: + os.remove(mozconfig_path) diff --git a/python/mozbuild/mozbuild/test/configure/data/decorators.configure b/python/mozbuild/mozbuild/test/configure/data/decorators.configure new file mode 100644 index 0000000000..b98eb26f3f --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/decorators.configure @@ -0,0 +1,53 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +@template +def simple_decorator(func): + return func + + +@template +def wrapper_decorator(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + +@template +def function_decorator(*args, **kwargs): + # We could return wrapper_decorator from above here, but then we wouldn't + # know if this works as expected because wrapper_decorator itself was + # modified or because the right thing happened here. + def wrapper_decorator(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + return wrapper_decorator + + +@depends("--help") +@simple_decorator +def foo(help): + global FOO + FOO = 1 + + +@depends("--help") +@wrapper_decorator +def bar(help): + global BAR + BAR = 1 + + +@depends("--help") +@function_decorator("a", "b", "c") +def qux(help): + global QUX + QUX = 1 diff --git a/python/mozbuild/mozbuild/test/configure/data/empty_mozconfig b/python/mozbuild/mozbuild/test/configure/data/empty_mozconfig new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/empty_mozconfig diff --git a/python/mozbuild/mozbuild/test/configure/data/extra.configure b/python/mozbuild/mozbuild/test/configure/data/extra.configure new file mode 100644 index 0000000000..e54a93dbc3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/extra.configure @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--extra", help="Extra") + + +@depends("--extra") +def extra(extra): + return extra + + +set_config("EXTRA", extra) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/imm.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/imm.configure new file mode 100644 index 0000000000..f20a4a7149 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/imm.configure @@ -0,0 +1,37 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +imply_option("--enable-foo", True) + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo", "--help") +def foo(value, help): + if value: + return True + + +imply_option("--enable-bar", ("foo", "bar")) + +option("--enable-bar", nargs="*", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +imply_option("--enable-baz", "BAZ") + +option("--enable-baz", nargs=1, help="enable baz") + + +@depends("--enable-baz") +def bar(value): + if value: + return value diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/infer.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer.configure new file mode 100644 index 0000000000..b73be9a720 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer.configure @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo", "--help") +def foo(value, help): + if value: + return True + + +imply_option("--enable-bar", foo) + + +option("--enable-bar", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/infer_ko.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer_ko.configure new file mode 100644 index 0000000000..9b3761c3c3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/infer_ko.configure @@ -0,0 +1,36 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--enable-hoge", help="enable hoge") + + +@depends("--enable-hoge") +def hoge(value): + return value + + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo", hoge) +def foo(value, hoge): + if value: + return True + + +imply_option("--enable-bar", foo) + + +option("--enable-bar", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/negative.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/negative.configure new file mode 100644 index 0000000000..e953231f5e --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/negative.configure @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo") +def foo(value): + if value: + return False + + +imply_option("--enable-bar", foo) + + +option("--disable-hoge", help="enable hoge") + + +@depends("--disable-hoge") +def hoge(value): + if not value: + return False + + +imply_option("--enable-bar", hoge) + + +option("--enable-bar", default=True, help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if not value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/simple.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/simple.configure new file mode 100644 index 0000000000..6aa225cc45 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/simple.configure @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--enable-foo", help="enable foo") + + +@depends("--enable-foo") +def foo(value): + if value: + return True + + +imply_option("--enable-bar", foo) + + +option("--enable-bar", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/imply_option/values.configure b/python/mozbuild/mozbuild/test/configure/data/imply_option/values.configure new file mode 100644 index 0000000000..93198a8295 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/imply_option/values.configure @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--enable-foo", nargs="*", help="enable foo") + + +@depends("--enable-foo") +def foo(value): + if value: + return value + + +imply_option("--enable-bar", foo) + + +option("--enable-bar", nargs="*", help="enable bar") + + +@depends("--enable-bar") +def bar(value): + if value: + return value + + +set_config("BAR", bar) diff --git a/python/mozbuild/mozbuild/test/configure/data/included.configure b/python/mozbuild/mozbuild/test/configure/data/included.configure new file mode 100644 index 0000000000..97166618ec --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/included.configure @@ -0,0 +1,68 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# For more complex and repetitive things, we can create templates +@template +def check_compiler_flag(flag): + @depends(is_gcc) + def check(value): + if value: + return [flag] + + set_config("CFLAGS", check) + return check + + +check_compiler_flag("-Werror=foobar") + +# Normal functions can be used in @depends functions. +def fortytwo(): + return 42 + + +def twentyone(): + yield 21 + + +@depends(is_gcc) +def check(value): + if value: + return fortytwo() + + +set_config("TEMPLATE_VALUE", check) + + +@depends(is_gcc) +def check(value): + if value: + for val in twentyone(): + return val + + +set_config("TEMPLATE_VALUE_2", check) + +# Normal functions can use @imports too to import modules. +@imports("sys") +def platform(): + return sys.platform + + +option("--enable-imports-in-template", help="Imports in template") + + +@depends("--enable-imports-in-template") +def check(value): + if value: + return platform() + + +set_config("PLATFORM", check) + + +@template +def indirectly_define_option(*args, **kwargs): + option(*args, **kwargs) diff --git a/python/mozbuild/mozbuild/test/configure/data/moz.configure b/python/mozbuild/mozbuild/test/configure/data/moz.configure new file mode 100644 index 0000000000..4d57eabbb9 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/moz.configure @@ -0,0 +1,205 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--enable-simple", help="Enable simple") + +# Setting MOZ_WITH_ENV in the environment has the same effect as passing +# --enable-with-env. +option("--enable-with-env", env="MOZ_WITH_ENV", help="Enable with env") + +# Optional values +option("--enable-values", nargs="*", help="Enable values") + +# Everything supported in the Option class is supported in option(). Assume +# the tests of the Option class are extensive about this. + +# Alternatively to --enable/--disable, there also is --with/--without. The +# difference is semantic only. Behavior is the same as --enable/--disable. + +# When the option name starts with --disable/--without, the default is for +# the option to be enabled. +option("--without-thing", help="Build without thing") + +# A --enable/--with option with a default of False is equivalent to a +# --disable/--without option. This can be used to change the defaults +# depending on e.g. the target or the built application. +option("--with-stuff", default=False, help="Build with stuff") + +# Other kinds of arbitrary options are also allowed. This is effectively +# equivalent to --enable/--with, with no possibility of --disable/--without. +option("--option", env="MOZ_OPTION", help="Option") + +# It is also possible to pass options through the environment only. +option(env="CC", nargs=1, help="C Compiler") + +# Call the function when the --enable-simple option is processed, with its +# OptionValue as argument. +@depends("--enable-simple") +def simple(simple): + if simple: + return simple + + +set_config("ENABLED_SIMPLE", simple) + +# There can be multiple functions depending on the same option. +@depends("--enable-simple") +def simple(simple): + return simple + + +set_config("SIMPLE", simple) + + +@depends("--enable-with-env") +def with_env(with_env): + return with_env + + +set_config("WITH_ENV", with_env) + +# It doesn't matter if the dependency is on --enable or --disable +@depends("--disable-values") +def with_env2(values): + return values + + +set_config("VALUES", with_env2) + +# It is possible to @depends on environment-only options. +@depends("CC") +def is_gcc(cc): + return cc and "gcc" in cc[0] + + +set_config("IS_GCC", is_gcc) + +# It is possible to depend on the result from another function. +@depends(with_env2) +def with_env3(values): + return values + + +set_config("VALUES2", with_env3) + +# @depends functions can also return results for use as input to another +# @depends. +@depends(with_env3) +def with_env4(values): + return values + + +@depends(with_env4) +def with_env5(values): + return values + + +set_config("VALUES3", with_env5) + +# The result from @depends functions can also be used as input to options. +# The result must be returned, not implied. +@depends("--enable-simple") +def simple(simple): + return "simple" if simple else "not-simple" + + +option("--with-returned-default", default=simple, help="Returned default") + + +@depends("--with-returned-default") +def default(value): + return value + + +set_config("DEFAULTED", default) + + +@depends("--enable-values") +def choices(values): + if len(values): + return { + "alpha": ("a", "b", "c"), + "numeric": ("0", "1", "2"), + }.get(values[0]) + + +option("--returned-choices", choices=choices, help="Choices") + + +@depends("--returned-choices") +def returned_choices(values): + return values + + +set_config("CHOICES", returned_choices) + +# All options must be referenced by some @depends function. +# It is possible to depend on multiple options/functions +@depends("--without-thing", "--with-stuff", with_env4, "--option") +def remainder(*args): + return args + + +set_config("REMAINDER", remainder) + +# It is possible to include other files to extend the configuration script. +include("included.configure") + +# It is also possible for the include file path to come from the result of a +# @depends function. +option("--enable-include", nargs=1, help="Include") + + +@depends("--enable-include") +def include_path(path): + return path[0] if path else None + + +include(include_path) + +# Sandboxed functions can import from modules through the use of the @imports +# decorator. +# The order of the decorators matter: @imports needs to appear after other +# decorators. +option("--with-imports", nargs="?", help="Imports") + +# A limited set of functions from os.path are exposed by default. +@depends("--with-imports") +def with_imports(value): + if len(value): + return hasattr(os.path, "abspath") + + +set_config("HAS_ABSPATH", with_imports) + +# It is still possible to import the full set from os.path. +# It is also possible to cherry-pick builtins. +@depends("--with-imports") +@imports("os.path") +def with_imports(value): + if len(value): + return hasattr(os.path, "getatime") + + +set_config("HAS_GETATIME", with_imports) + + +@depends("--with-imports") +def with_imports(value): + if len(value): + return hasattr(os.path, "getatime") + + +set_config("HAS_GETATIME2", with_imports) + +# This option should be attributed to this file in the --help output even though +# included.configure is the actual file that defines the option. +indirectly_define_option("--indirect-option", help="Indirectly defined option") + + +@depends("--indirect-option") +def indirect_option(option): + return option diff --git a/python/mozbuild/mozbuild/test/configure/data/set_config.configure b/python/mozbuild/mozbuild/test/configure/data/set_config.configure new file mode 100644 index 0000000000..0ae5fef6d6 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/set_config.configure @@ -0,0 +1,51 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--set-foo", help="set foo") + + +@depends("--set-foo") +def foo(value): + if value: + return True + + +set_config("FOO", foo) + + +option("--set-bar", help="set bar") + + +@depends("--set-bar") +def bar(value): + return bool(value) + + +set_config("BAR", bar) + + +option("--set-value", nargs=1, help="set value") + + +@depends("--set-value") +def set_value(value): + if value: + return value[0] + + +set_config("VALUE", set_value) + + +option("--set-name", nargs=1, help="set name") + + +@depends("--set-name") +def set_name(value): + if value: + return value[0] + + +set_config(set_name, True) diff --git a/python/mozbuild/mozbuild/test/configure/data/set_define.configure b/python/mozbuild/mozbuild/test/configure/data/set_define.configure new file mode 100644 index 0000000000..ce9a60d7f1 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/set_define.configure @@ -0,0 +1,51 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +option("--set-foo", help="set foo") + + +@depends("--set-foo") +def foo(value): + if value: + return True + + +set_define("FOO", foo) + + +option("--set-bar", help="set bar") + + +@depends("--set-bar") +def bar(value): + return bool(value) + + +set_define("BAR", bar) + + +option("--set-value", nargs=1, help="set value") + + +@depends("--set-value") +def set_value(value): + if value: + return value[0] + + +set_define("VALUE", set_value) + + +option("--set-name", nargs=1, help="set name") + + +@depends("--set-name") +def set_name(value): + if value: + return value[0] + + +set_define(set_name, True) diff --git a/python/mozbuild/mozbuild/test/configure/data/subprocess.configure b/python/mozbuild/mozbuild/test/configure/data/subprocess.configure new file mode 100644 index 0000000000..3316fee087 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/data/subprocess.configure @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +@depends("--help") +@imports("codecs") +@imports(_from="mozbuild.configure.util", _import="getpreferredencoding") +@imports("os") +@imports(_from="__builtin__", _import="open") +def dies_when_logging(_): + test_file = "test.txt" + quote_char = "'" + if getpreferredencoding().lower() == "utf-8": + quote_char = "\u00B4" + try: + with open(test_file, "w+") as fh: + fh.write(quote_char) + out = check_cmd_output("cat", "test.txt") + log.info(out) + finally: + os.remove(test_file) diff --git a/python/mozbuild/mozbuild/test/configure/lint.py b/python/mozbuild/mozbuild/test/configure/lint.py new file mode 100644 index 0000000000..59d41da264 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/lint.py @@ -0,0 +1,62 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import unittest + +import six +from buildconfig import topobjdir, topsrcdir +from mozunit import main + +from mozbuild.configure.lint import LintSandbox + +test_path = os.path.abspath(__file__) + + +class LintMeta(type): + def __new__(mcs, name, bases, attrs): + def create_test(project, func): + def test(self): + return func(self, project) + + return test + + for project in ( + "browser", + "js", + "memory", + "mobile/android", + ): + attrs["test_%s" % project.replace("/", "_")] = create_test( + project, attrs["lint"] + ) + + return type.__new__(mcs, name, bases, attrs) + + +# We don't actually need python2 compat, but this makes flake8 happy. +@six.add_metaclass(LintMeta) +class Lint(unittest.TestCase): + def setUp(self): + self._curdir = os.getcwd() + os.chdir(topobjdir) + + def tearDown(self): + os.chdir(self._curdir) + + def lint(self, project): + sandbox = LintSandbox( + { + "OLD_CONFIGURE": os.path.join(topsrcdir, "old-configure"), + "MOZCONFIG": os.path.join( + os.path.dirname(test_path), "data", "empty_mozconfig" + ), + }, + ["configure", "--enable-project=%s" % project, "--help"], + ) + sandbox.run(os.path.join(topsrcdir, "moz.configure")) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/macos_fake_sdk/SDKSettings.plist b/python/mozbuild/mozbuild/test/configure/macos_fake_sdk/SDKSettings.plist new file mode 100644 index 0000000000..f0d6e1949f --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/macos_fake_sdk/SDKSettings.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Version</key> + <string>13.3</string> +</dict> +</plist> diff --git a/python/mozbuild/mozbuild/test/configure/test_bootstrap.py b/python/mozbuild/mozbuild/test/configure/test_bootstrap.py new file mode 100644 index 0000000000..eaa417d566 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_bootstrap.py @@ -0,0 +1,43 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from mozunit import main + +from common import BaseConfigureTest + + +class TestBootstrap(BaseConfigureTest): + def test_bootstrap(self): + def get_value_for(arg): + sandbox = self.get_sandbox({}, {}, [arg], {}) + return sandbox._value_for(sandbox["enable_bootstrap"]) + + self.assertEqual(None, get_value_for("--disable-bootstrap")) + + # With `--enable-bootstrap`, anything is bootstrappable + bootstrap = get_value_for("--enable-bootstrap") + self.assertTrue(bootstrap("foo")) + self.assertTrue(bootstrap("bar")) + + # With `--enable-bootstrap=foo,bar`, only foo and bar are bootstrappable + bootstrap = get_value_for("--enable-bootstrap=foo,bar") + self.assertTrue(bootstrap("foo")) + self.assertTrue(bootstrap("bar")) + self.assertFalse(bootstrap("qux")) + + # With `--enable-bootstrap=-foo`, anything is bootstrappable, except foo + bootstrap = get_value_for("--enable-bootstrap=-foo") + self.assertFalse(bootstrap("foo")) + self.assertTrue(bootstrap("bar")) + self.assertTrue(bootstrap("qux")) + + # Corner case. + bootstrap = get_value_for("--enable-bootstrap=-foo,foo,bar") + self.assertFalse(bootstrap("foo")) + self.assertTrue(bootstrap("bar")) + self.assertFalse(bootstrap("qux")) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_checks_configure.py b/python/mozbuild/mozbuild/test/configure/test_checks_configure.py new file mode 100644 index 0000000000..53361ff199 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_checks_configure.py @@ -0,0 +1,1169 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +import textwrap +import unittest + +from buildconfig import topsrcdir +from mozpack import path as mozpath +from mozunit import MockedOpen, main +from six import StringIO + +from common import ConfigureTestSandbox, ensure_exe_extension, fake_short_path +from mozbuild.configure import ConfigureError, ConfigureSandbox +from mozbuild.shellutil import quote as shell_quote +from mozbuild.util import exec_ + + +class TestChecksConfigure(unittest.TestCase): + def test_checking(self): + def make_test(to_exec): + def test(val, msg): + out = StringIO() + sandbox = ConfigureSandbox({}, stdout=out, stderr=out) + base_dir = os.path.join(topsrcdir, "build", "moz.configure") + sandbox.include_file(os.path.join(base_dir, "checks.configure")) + exec_(to_exec, sandbox) + sandbox["foo"](val) + self.assertEqual(out.getvalue(), msg) + + return test + + test = make_test( + textwrap.dedent( + """ + @checking('for a thing') + def foo(value): + return value + """ + ) + ) + test(True, "checking for a thing... yes\n") + test(False, "checking for a thing... no\n") + test(42, "checking for a thing... 42\n") + test("foo", "checking for a thing... foo\n") + data = ["foo", "bar"] + test(data, "checking for a thing... %r\n" % data) + + # When the function given to checking does nothing interesting, the + # behavior is not altered + test = make_test( + textwrap.dedent( + """ + @checking('for a thing', lambda x: x) + def foo(value): + return value + """ + ) + ) + test(True, "checking for a thing... yes\n") + test(False, "checking for a thing... no\n") + test(42, "checking for a thing... 42\n") + test("foo", "checking for a thing... foo\n") + data = ["foo", "bar"] + test(data, "checking for a thing... %r\n" % data) + + test = make_test( + textwrap.dedent( + """ + def munge(x): + if not x: + return 'not found' + if isinstance(x, (str, bool, int)): + return x + return ' '.join(x) + + @checking('for a thing', munge) + def foo(value): + return value + """ + ) + ) + test(True, "checking for a thing... yes\n") + test(False, "checking for a thing... not found\n") + test(42, "checking for a thing... 42\n") + test("foo", "checking for a thing... foo\n") + data = ["foo", "bar"] + test(data, "checking for a thing... foo bar\n") + + KNOWN_A = ensure_exe_extension(mozpath.abspath("/usr/bin/known-a")) + KNOWN_B = ensure_exe_extension(mozpath.abspath("/usr/local/bin/known-b")) + KNOWN_C = ensure_exe_extension(mozpath.abspath("/home/user/bin/known c")) + OTHER_A = ensure_exe_extension(mozpath.abspath("/lib/other/known-a")) + + def get_result( + self, + command="", + args=[], + environ={}, + prog="/bin/configure", + extra_paths=None, + includes=("util.configure", "checks.configure"), + ): + config = {} + out = StringIO() + paths = {self.KNOWN_A: None, self.KNOWN_B: None, self.KNOWN_C: None} + if extra_paths: + paths.update(extra_paths) + environ = dict(environ) + if "PATH" not in environ: + environ["PATH"] = os.pathsep.join(os.path.dirname(p) for p in paths) + paths[self.OTHER_A] = None + sandbox = ConfigureTestSandbox(paths, config, environ, [prog] + args, out, out) + base_dir = os.path.join(topsrcdir, "build", "moz.configure") + for f in includes: + sandbox.include_file(os.path.join(base_dir, f)) + + status = 0 + try: + exec_(command, sandbox) + sandbox.run() + except SystemExit as e: + status = e.code + + return config, out.getvalue(), status + + def test_check_prog(self): + config, out, status = self.get_result('check_prog("FOO", ("known-a",))') + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))' + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_B}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_B) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "unknown-2", "known c"))' + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": fake_short_path(self.KNOWN_C)}) + self.assertEqual( + out, "checking for foo... %s\n" % shell_quote(fake_short_path(self.KNOWN_C)) + ) + + config, out, status = self.get_result('check_prog("FOO", ("unknown",))') + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo... not found + DEBUG: foo: Looking for unknown + ERROR: Cannot find foo + """ + ), + ) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "unknown-2", "unknown 3"))' + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo... not found + DEBUG: foo: Looking for unknown + DEBUG: foo: Looking for unknown-2 + DEBUG: foo: Looking for 'unknown 3' + ERROR: Cannot find foo + """ + ), + ) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "unknown-2", "unknown 3"), ' + "allow_missing=True)" + ) + self.assertEqual(status, 0) + self.assertEqual(config, {}) + self.assertEqual(out, "checking for foo... not found\n") + + @unittest.skipIf(not sys.platform.startswith("win"), "Windows-only test") + def test_check_prog_exe(self): + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', ["FOO=known-a.exe"] + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', + ["FOO=%s" % os.path.splitext(self.KNOWN_A)[0]], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + def test_check_prog_with_args(self): + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', ["FOO=known-a"] + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', + ["FOO=%s" % self.KNOWN_A], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + path = self.KNOWN_B.replace("known-b", "known-a") + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "known-b", "known c"))', ["FOO=%s" % path] + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo... not found + DEBUG: foo: Looking for %s + ERROR: Cannot find foo + """ + ) + % path, + ) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown",))', ["FOO=known c"] + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": fake_short_path(self.KNOWN_C)}) + self.assertEqual( + out, "checking for foo... %s\n" % shell_quote(fake_short_path(self.KNOWN_C)) + ) + + config, out, status = self.get_result( + 'check_prog("FOO", ("unknown", "unknown-2", "unknown 3"), ' + "allow_missing=True)", + ["FOO=unknown"], + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo... not found + DEBUG: foo: Looking for unknown + ERROR: Cannot find foo + """ + ), + ) + + def test_check_prog_what(self): + config, out, status = self.get_result( + 'check_prog("CC", ("known-a",), what="the target C compiler")' + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_A}) + self.assertEqual( + out, "checking for the target C compiler... %s\n" % self.KNOWN_A + ) + + config, out, status = self.get_result( + 'check_prog("CC", ("unknown", "unknown-2", "unknown 3"),' + ' what="the target C compiler")' + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for the target C compiler... not found + DEBUG: cc: Looking for unknown + DEBUG: cc: Looking for unknown-2 + DEBUG: cc: Looking for 'unknown 3' + ERROR: Cannot find the target C compiler + """ + ), + ) + + def test_check_prog_input(self): + config, out, status = self.get_result( + textwrap.dedent( + """ + option("--with-ccache", nargs=1, help="ccache") + check_prog("CCACHE", ("known-a",), input="--with-ccache") + """ + ), + ["--with-ccache=known-b"], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"CCACHE": self.KNOWN_B}) + self.assertEqual(out, "checking for ccache... %s\n" % self.KNOWN_B) + + script = textwrap.dedent( + """ + option(env="CC", nargs=1, help="compiler") + @depends("CC") + def compiler(value): + return value[0].split()[0] if value else None + check_prog("CC", ("known-a",), input=compiler) + """ + ) + config, out, status = self.get_result(script) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_A}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result(script, ["CC=known-b"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_B}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_B) + + config, out, status = self.get_result(script, ["CC=known-b -m32"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_B}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_B) + + def test_check_prog_progs(self): + config, out, status = self.get_result('check_prog("FOO", ())') + self.assertEqual(status, 0) + self.assertEqual(config, {}) + self.assertEqual(out, "") + + config, out, status = self.get_result('check_prog("FOO", ())', ["FOO=known-a"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"FOO": self.KNOWN_A}) + self.assertEqual(out, "checking for foo... %s\n" % self.KNOWN_A) + + script = textwrap.dedent( + """ + option(env="TARGET", nargs=1, default="linux", help="target") + @depends("TARGET") + def compiler(value): + if value: + if value[0] == "linux": + return ("gcc", "clang") + if value[0] == "winnt": + return ("cl", "clang-cl") + check_prog("CC", compiler) + """ + ) + config, out, status = self.get_result(script) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for cc... not found + DEBUG: cc: Looking for gcc + DEBUG: cc: Looking for clang + ERROR: Cannot find cc + """ + ), + ) + + config, out, status = self.get_result(script, ["TARGET=linux"]) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for cc... not found + DEBUG: cc: Looking for gcc + DEBUG: cc: Looking for clang + ERROR: Cannot find cc + """ + ), + ) + + config, out, status = self.get_result(script, ["TARGET=winnt"]) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for cc... not found + DEBUG: cc: Looking for cl + DEBUG: cc: Looking for clang-cl + ERROR: Cannot find cc + """ + ), + ) + + config, out, status = self.get_result(script, ["TARGET=none"]) + self.assertEqual(status, 0) + self.assertEqual(config, {}) + self.assertEqual(out, "") + + config, out, status = self.get_result(script, ["TARGET=winnt", "CC=known-a"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_A}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_A) + + config, out, status = self.get_result(script, ["TARGET=none", "CC=known-a"]) + self.assertEqual(status, 0) + self.assertEqual(config, {"CC": self.KNOWN_A}) + self.assertEqual(out, "checking for cc... %s\n" % self.KNOWN_A) + + def test_check_prog_configure_error(self): + with self.assertRaises(ConfigureError) as e: + self.get_result('check_prog("FOO", "foo")') + + self.assertEqual(str(e.exception), "progs must resolve to a list or tuple!") + + with self.assertRaises(ConfigureError) as e: + self.get_result( + 'foo = depends(when=True)(lambda: ("a", "b"))\n' + 'check_prog("FOO", ("known-a",), input=foo)' + ) + + self.assertEqual( + str(e.exception), + "input must resolve to a tuple or a list with a " + "single element, or a string", + ) + + with self.assertRaises(ConfigureError) as e: + self.get_result( + 'foo = depends(when=True)(lambda: {"a": "b"})\n' + 'check_prog("FOO", ("known-a",), input=foo)' + ) + + self.assertEqual( + str(e.exception), + "input must resolve to a tuple or a list with a " + "single element, or a string", + ) + + def test_check_prog_with_path(self): + config, out, status = self.get_result( + 'check_prog("A", ("known-a",), paths=["/some/path"])' + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... not found + DEBUG: a: Looking for known-a + ERROR: Cannot find a + """ + ), + ) + + config, out, status = self.get_result( + 'check_prog("A", ("known-a",), paths=["%s"])' + % os.path.dirname(self.OTHER_A) + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"A": self.OTHER_A}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... %s + """ + % self.OTHER_A + ), + ) + + dirs = map(mozpath.dirname, (self.OTHER_A, self.KNOWN_A)) + config, out, status = self.get_result( + textwrap.dedent( + """\ + check_prog("A", ("known-a",), paths=["%s"]) + """ + % os.pathsep.join(dirs) + ) + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"A": self.OTHER_A}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... %s + """ + % self.OTHER_A + ), + ) + + dirs = map(mozpath.dirname, (self.KNOWN_A, self.KNOWN_B)) + config, out, status = self.get_result( + textwrap.dedent( + """\ + check_prog("A", ("known-a",), paths=["%s", "%s"]) + """ + % (os.pathsep.join(dirs), self.OTHER_A) + ) + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"A": self.KNOWN_A}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... %s + """ + % self.KNOWN_A + ), + ) + + config, out, status = self.get_result( + 'check_prog("A", ("known-a",), paths="%s")' % os.path.dirname(self.OTHER_A) + ) + + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for a... """ # noqa # trailing whitespace... + """ + DEBUG: a: Looking for known-a + ERROR: Paths provided to find_program must be a list of strings, not %r + """ + % mozpath.dirname(self.OTHER_A) + ), + ) + + @unittest.skipIf( + not sys.platform.startswith("linux"), + "Linux-only test, assumes Java is located from a $PATH", + ) + def test_java_tool_checks_linux(self): + def run_configure_java( + mock_fs_paths, mock_java_home=None, mock_path=None, args=[] + ): + script = textwrap.dedent( + """\ + @depends('--help') + def host(_): + return namespace(os='unknown', kernel='unknown') + toolchains_base_dir = depends(when=True)(lambda: '/mozbuild') + include('%(topsrcdir)s/build/moz.configure/java.configure') + """ + % {"topsrcdir": topsrcdir} + ) + + # Don't let system JAVA_HOME influence the test + original_java_home = os.environ.pop("JAVA_HOME", None) + configure_environ = {} + + if mock_java_home: + os.environ["JAVA_HOME"] = mock_java_home + configure_environ["JAVA_HOME"] = mock_java_home + + if mock_path: + configure_environ["PATH"] = mock_path + + # * Even if the real file sysphabtem has a symlink at the mocked path, don't let + # realpath follow it, as it may influence the test. + # * When finding a binary, check the mock paths rather than the real filesystem. + # Note: Python doesn't allow the different "with" bits to be put in parenthesis, + # because then it thinks it's an un-with-able tuple. Additionally, if this is cleanly + # lined up with "\", black removes them and autoformats them to the block that is + # below. + result = self.get_result( + args=args, + command=script, + extra_paths=paths, + environ=configure_environ, + ) + + if original_java_home: + os.environ["JAVA_HOME"] = original_java_home + return result + + java = mozpath.abspath("/usr/bin/java") + javac = mozpath.abspath("/usr/bin/javac") + paths = {java: None, javac: None} + expected_error_message = ( + "ERROR: Could not locate Java at /mozbuild/jdk/jdk-17.0.7+7/bin, " + "please run ./mach bootstrap --no-system-changes\n" + ) + + config, out, status = run_configure_java(paths) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual(out, expected_error_message) + + # An alternative valid set of tools referred to by JAVA_HOME. + alt_java = mozpath.abspath("/usr/local/bin/java") + alt_javac = mozpath.abspath("/usr/local/bin/javac") + alt_java_home = mozpath.dirname(mozpath.dirname(alt_java)) + paths = {alt_java: None, alt_javac: None, java: None, javac: None} + + alt_path = mozpath.dirname(java) + config, out, status = run_configure_java(paths, alt_java_home, alt_path) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual(out, expected_error_message) + + # We can use --with-java-bin-path instead of JAVA_HOME to similar + # effect. + config, out, status = run_configure_java( + paths, + mock_path=mozpath.dirname(java), + args=["--with-java-bin-path=%s" % mozpath.dirname(alt_java)], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"JAVA": alt_java, "MOZ_JAVA_CODE_COVERAGE": False}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for java... %s + """ + % alt_java + ), + ) + + # If --with-java-bin-path and JAVA_HOME are both set, + # --with-java-bin-path takes precedence. + config, out, status = run_configure_java( + paths, + mock_java_home=mozpath.dirname(mozpath.dirname(java)), + mock_path=mozpath.dirname(java), + args=["--with-java-bin-path=%s" % mozpath.dirname(alt_java)], + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"JAVA": alt_java, "MOZ_JAVA_CODE_COVERAGE": False}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for java... %s + """ + % alt_java + ), + ) + + # --enable-java-coverage should set MOZ_JAVA_CODE_COVERAGE. + alt_java_home = mozpath.dirname(mozpath.dirname(java)) + config, out, status = run_configure_java( + paths, + mock_java_home=alt_java_home, + mock_path=mozpath.dirname(java), + args=["--enable-java-coverage"], + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + + # Any missing tool is fatal when these checks run. + paths = {} + config, out, status = run_configure_java( + mock_fs_paths={}, + mock_path=mozpath.dirname(java), + args=["--enable-java-coverage"], + ) + self.assertEqual(status, 1) + self.assertEqual(config, {}) + self.assertEqual(out, expected_error_message) + + def test_pkg_check_modules(self): + mock_pkg_config_version = "0.10.0" + mock_pkg_config_path = mozpath.abspath("/usr/bin/pkg-config") + + seen_flags = set() + + def mock_pkg_config(_, args): + if "--dont-define-prefix" in args: + args = list(args) + seen_flags.add(args.pop(args.index("--dont-define-prefix"))) + args = tuple(args) + if args[0:2] == ("--errors-to-stdout", "--print-errors"): + assert len(args) == 3 + package = args[2] + if package == "unknown": + return ( + 1, + "Package unknown was not found in the pkg-config search path.\n" + "Perhaps you should add the directory containing `unknown.pc'\n" + "to the PKG_CONFIG_PATH environment variable\n" + "No package 'unknown' found", + "", + ) + if package == "valid": + return 0, "", "" + if package == "new > 1.1": + return 1, "Requested 'new > 1.1' but version of new is 1.1", "" + if args[0] == "--cflags": + assert len(args) == 2 + return 0, "-I/usr/include/%s" % args[1], "" + if args[0] == "--libs": + assert len(args) == 2 + return 0, "-l%s" % args[1], "" + if args[0] == "--version": + return 0, mock_pkg_config_version, "" + if args[0] == "--about": + return 1, "Unknown option --about", "" + self.fail("Unexpected arguments to mock_pkg_config: %s" % (args,)) + + def mock_pkgconf(_, args): + if args[0] == "--shared": + seen_flags.add(args[0]) + args = args[1:] + if args[0] == "--about": + return 0, "pkgconf {}".format(mock_pkg_config_version), "" + return mock_pkg_config(_, args) + + def get_result(cmd, args=[], bootstrapped_sysroot=False, extra_paths=None): + return self.get_result( + textwrap.dedent( + """\ + option('--disable-compile-environment', help='compile env') + compile_environment = depends(when='--enable-compile-environment')(lambda: True) + toolchain_prefix = depends(when=True)(lambda: None) + target_multiarch_dir = depends(when=True)(lambda: None) + target_sysroot = depends(when=True)(lambda: %(sysroot)s) + target = depends(when=True)(lambda: None) + include('%(topsrcdir)s/build/moz.configure/util.configure') + include('%(topsrcdir)s/build/moz.configure/checks.configure') + # Skip bootstrapping. + @template + def check_prog(*args, **kwargs): + del kwargs["bootstrap"] + return check_prog(*args, **kwargs) + include('%(topsrcdir)s/build/moz.configure/pkg.configure') + """ + % { + "topsrcdir": topsrcdir, + "sysroot": "namespace(bootstrapped=True)" + if bootstrapped_sysroot + else "None", + } + ) + + cmd, + args=args, + extra_paths=extra_paths, + includes=(), + ) + + extra_paths = {mock_pkg_config_path: mock_pkg_config} + + config, output, status = get_result("pkg_check_modules('MOZ_VALID', 'valid')") + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... not found + ERROR: *** The pkg-config script could not be found. Make sure it is + *** in your path, or set the PKG_CONFIG environment variable + *** to the full path to pkg-config. + """ + ), + ) + + for pkg_config, version, bootstrapped_sysroot, is_pkgconf in ( + (mock_pkg_config, "0.10.0", False, False), + (mock_pkg_config, "0.30.0", False, False), + (mock_pkg_config, "0.30.0", True, False), + (mock_pkgconf, "1.1.0", True, True), + (mock_pkgconf, "1.6.0", False, True), + (mock_pkgconf, "1.8.0", False, True), + (mock_pkgconf, "1.8.0", True, True), + ): + seen_flags = set() + mock_pkg_config_version = version + config, output, status = get_result( + "pkg_check_modules('MOZ_VALID', 'valid')", + bootstrapped_sysroot=bootstrapped_sysroot, + extra_paths={mock_pkg_config_path: pkg_config}, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... %s + checking whether pkg-config is pkgconf... %s + checking for valid... yes + checking MOZ_VALID_CFLAGS... -I/usr/include/valid + checking MOZ_VALID_LIBS... -lvalid + """ + % ( + mock_pkg_config_path, + mock_pkg_config_version, + "yes" if is_pkgconf else "no", + ) + ), + ) + self.assertEqual( + config, + { + "PKG_CONFIG": mock_pkg_config_path, + "MOZ_VALID_CFLAGS": ("-I/usr/include/valid",), + "MOZ_VALID_LIBS": ("-lvalid",), + }, + ) + if version == "1.8.0" and bootstrapped_sysroot: + self.assertEqual(seen_flags, set(["--shared", "--dont-define-prefix"])) + elif version == "1.8.0": + self.assertEqual(seen_flags, set(["--shared"])) + elif version in ("1.6.0", "0.30.0") and bootstrapped_sysroot: + self.assertEqual(seen_flags, set(["--dont-define-prefix"])) + else: + self.assertEqual(seen_flags, set()) + + config, output, status = get_result( + "pkg_check_modules('MOZ_UKNOWN', 'unknown')", extra_paths=extra_paths + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... %s + checking whether pkg-config is pkgconf... no + checking for unknown... no + ERROR: Package unknown was not found in the pkg-config search path. + ERROR: Perhaps you should add the directory containing `unknown.pc' + ERROR: to the PKG_CONFIG_PATH environment variable + ERROR: No package 'unknown' found + """ + % (mock_pkg_config_path, mock_pkg_config_version) + ), + ) + self.assertEqual(config, {"PKG_CONFIG": mock_pkg_config_path}) + + config, output, status = get_result( + "pkg_check_modules('MOZ_NEW', 'new > 1.1')", extra_paths=extra_paths + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... %s + checking whether pkg-config is pkgconf... no + checking for new > 1.1... no + ERROR: Requested 'new > 1.1' but version of new is 1.1 + """ + % (mock_pkg_config_path, mock_pkg_config_version) + ), + ) + self.assertEqual(config, {"PKG_CONFIG": mock_pkg_config_path}) + + # allow_missing makes missing packages non-fatal. + cmd = textwrap.dedent( + """\ + have_new_module = pkg_check_modules('MOZ_NEW', 'new > 1.1', allow_missing=True) + @depends(have_new_module) + def log_new_module_error(mod): + if mod is not True: + log.info('Module not found.') + """ + ) + + config, output, status = get_result(cmd, extra_paths=extra_paths) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... %s + checking whether pkg-config is pkgconf... no + checking for new > 1.1... no + WARNING: Requested 'new > 1.1' but version of new is 1.1 + Module not found. + """ + % (mock_pkg_config_path, mock_pkg_config_version) + ), + ) + self.assertEqual(config, {"PKG_CONFIG": mock_pkg_config_path}) + + config, output, status = get_result( + cmd, args=["--disable-compile-environment"], extra_paths=extra_paths + ) + self.assertEqual(status, 0) + self.assertEqual(output, "Module not found.\n") + self.assertEqual(config, {}) + + def mock_old_pkg_config(_, args): + if args[0] == "--version": + return 0, "0.8.10", "" + if args[0] == "--about": + return 1, "Unknown option --about", "" + self.fail("Unexpected arguments to mock_old_pkg_config: %s" % args) + + extra_paths = {mock_pkg_config_path: mock_old_pkg_config} + + config, output, status = get_result( + "pkg_check_modules('MOZ_VALID', 'valid')", extra_paths=extra_paths + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for pkg_config... %s + checking for pkg-config version... 0.8.10 + checking whether pkg-config is pkgconf... no + ERROR: *** Your version of pkg-config is too old. You need version 0.9.0 or newer. + """ + % mock_pkg_config_path + ), + ) + + def test_simple_keyfile(self): + includes = ("util.configure", "checks.configure", "keyfiles.configure") + + config, output, status = self.get_result( + "simple_keyfile('Mozilla API')", includes=includes + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... no + """ + ), + ) + self.assertEqual(config, {"MOZ_MOZILLA_API_KEY": "no-mozilla-api-key"}) + + config, output, status = self.get_result( + "simple_keyfile('Mozilla API')", + args=["--with-mozilla-api-keyfile=/foo/bar/does/not/exist"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... no + ERROR: '/foo/bar/does/not/exist': No such file or directory. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"key": ""}): + config, output, status = self.get_result( + "simple_keyfile('Mozilla API')", + args=["--with-mozilla-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... no + ERROR: 'key' is empty. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"key": "fake-key\n"}): + config, output, status = self.get_result( + "simple_keyfile('Mozilla API')", + args=["--with-mozilla-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... yes + """ + ), + ) + self.assertEqual(config, {"MOZ_MOZILLA_API_KEY": "fake-key"}) + + with MockedOpen({"default": "default-key\n"}): + config, output, status = self.get_result( + "simple_keyfile('Mozilla API', default='default')", includes=includes + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... yes + """ + ), + ) + self.assertEqual(config, {"MOZ_MOZILLA_API_KEY": "default-key"}) + + with MockedOpen({"default": "default-key\n", "key": "fake-key\n"}): + config, output, status = self.get_result( + "simple_keyfile('Mozilla API', default='key')", includes=includes + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Mozilla API key... yes + """ + ), + ) + self.assertEqual(config, {"MOZ_MOZILLA_API_KEY": "fake-key"}) + + def test_id_and_secret_keyfile(self): + includes = ("util.configure", "checks.configure", "keyfiles.configure") + + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", includes=includes + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... no + """ + ), + ) + self.assertEqual( + config, + { + "MOZ_BING_API_CLIENTID": "no-bing-api-clientid", + "MOZ_BING_API_KEY": "no-bing-api-key", + }, + ) + + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", + args=["--with-bing-api-keyfile=/foo/bar/does/not/exist"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... no + ERROR: '/foo/bar/does/not/exist': No such file or directory. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"key": ""}): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", + args=["--with-bing-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... no + ERROR: 'key' is empty. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"key": "fake-id fake-key\n"}): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", + args=["--with-bing-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... yes + """ + ), + ) + self.assertEqual( + config, + {"MOZ_BING_API_CLIENTID": "fake-id", "MOZ_BING_API_KEY": "fake-key"}, + ) + + with MockedOpen({"key": "fake-key\n"}): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API')", + args=["--with-bing-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 1) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... no + ERROR: Bing API key file has an invalid format. + """ + ), + ) + self.assertEqual(config, {}) + + with MockedOpen({"default-key": "default-id default-key\n"}): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API', default='default-key')", + includes=includes, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... yes + """ + ), + ) + self.assertEqual( + config, + { + "MOZ_BING_API_CLIENTID": "default-id", + "MOZ_BING_API_KEY": "default-key", + }, + ) + + with MockedOpen( + {"default-key": "default-id default-key\n", "key": "fake-id fake-key\n"} + ): + config, output, status = self.get_result( + "id_and_secret_keyfile('Bing API', default='default-key')", + args=["--with-bing-api-keyfile=key"], + includes=includes, + ) + self.assertEqual(status, 0) + self.assertEqual( + output, + textwrap.dedent( + """\ + checking for the Bing API key... yes + """ + ), + ) + self.assertEqual( + config, + {"MOZ_BING_API_CLIENTID": "fake-id", "MOZ_BING_API_KEY": "fake-key"}, + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_compile_checks.py b/python/mozbuild/mozbuild/test/configure/test_compile_checks.py new file mode 100644 index 0000000000..37988d535f --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_compile_checks.py @@ -0,0 +1,599 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import textwrap +import unittest + +import mozpack.path as mozpath +from buildconfig import topsrcdir +from mozunit import main +from six import StringIO +from test_toolchain_helpers import FakeCompiler + +from common import ConfigureTestSandbox +from mozbuild.util import exec_ + + +class BaseCompileChecks(unittest.TestCase): + def get_mock_compiler(self, expected_test_content=None, expected_flags=None): + expected_flags = expected_flags or [] + + def mock_compiler(stdin, args): + if args != ["--version"]: + test_file = [a for a in args if not a.startswith("-")] + self.assertEqual(len(test_file), 1) + test_file = test_file[0] + args = [a for a in args if a.startswith("-")] + self.assertIn("-c", args) + for flag in expected_flags: + self.assertIn(flag, args) + + if expected_test_content: + with open(test_file) as fh: + test_content = fh.read() + self.assertEqual(test_content, expected_test_content) + + return FakeCompiler()(None, args) + + return mock_compiler + + def do_compile_test(self, command, expected_test_content=None, expected_flags=None): + + paths = { + os.path.abspath("/usr/bin/mockcc"): self.get_mock_compiler( + expected_test_content=expected_test_content, + expected_flags=expected_flags, + ), + } + + base_dir = os.path.join(topsrcdir, "build", "moz.configure") + + mock_compiler_defs = textwrap.dedent( + """\ + @depends(when=True) + def extra_toolchain_flags(): + return [] + + @depends(when=True) + def linker_ldflags(): + return [] + + target = depends(when=True)(lambda: True) + + @depends(when=True) + def configure_cache(): + + class ConfigureCache(dict): + pass + + cache_data = {} + + cache = ConfigureCache(cache_data) + cache.version_checked_compilers = set() + + return cache + + include('%s/compilers-util.configure') + + @template + def wrap_compiler(compiler): + return compiler_class(compiler, False) + + @wrap_compiler + @depends(when=True) + def c_compiler(): + return namespace( + flags=[], + type='gcc', + compiler=os.path.abspath('/usr/bin/mockcc'), + wrapper=[], + language='C', + ) + + @wrap_compiler + @depends(when=True) + def host_c_compiler(): + return namespace( + flags=[], + type='gcc', + compiler=os.path.abspath('/usr/bin/mockcc'), + wrapper=[], + language='C', + ) + + @wrap_compiler + @depends(when=True) + def cxx_compiler(): + return namespace( + flags=[], + type='gcc', + compiler=os.path.abspath('/usr/bin/mockcc'), + wrapper=[], + language='C++', + ) + + @wrap_compiler + @depends(when=True) + def host_cxx_compiler(): + return namespace( + flags=[], + type='gcc', + compiler=os.path.abspath('/usr/bin/mockcc'), + wrapper=[], + language='C++', + ) + """ + % mozpath.normsep(base_dir) + ) + + config = {} + out = StringIO() + sandbox = ConfigureTestSandbox(paths, config, {}, ["/bin/configure"], out, out) + sandbox.include_file(os.path.join(base_dir, "util.configure")) + sandbox.include_file(os.path.join(base_dir, "checks.configure")) + exec_(mock_compiler_defs, sandbox) + sandbox.include_file(os.path.join(base_dir, "compile-checks.configure")) + + status = 0 + try: + exec_(command, sandbox) + sandbox.run() + except SystemExit as e: + status = e.code + + return config, out.getvalue(), status + + +class TestHeaderChecks(BaseCompileChecks): + def test_try_compile_include(self): + expected_test_content = textwrap.dedent( + """\ + #include <foo.h> + #include <bar.h> + int + main(void) + { + + ; + return 0; + } + """ + ) + + cmd = textwrap.dedent( + """\ + try_compile(['foo.h', 'bar.h'], language='C') + """ + ) + + config, out, status = self.do_compile_test(cmd, expected_test_content) + self.assertEqual(status, 0) + self.assertEqual(config, {}) + + def test_try_compile_flags(self): + expected_flags = ["--extra", "--flags"] + + cmd = textwrap.dedent( + """\ + try_compile(language='C++', flags=['--flags', '--extra']) + """ + ) + + config, out, status = self.do_compile_test(cmd, expected_flags=expected_flags) + self.assertEqual(status, 0) + self.assertEqual(config, {}) + + def test_try_compile_failure(self): + cmd = textwrap.dedent( + """\ + have_fn = try_compile(body='somefn();', flags=['-funknown-flag']) + set_config('HAVE_SOMEFN', have_fn) + + have_another = try_compile(body='anotherfn();', language='C') + set_config('HAVE_ANOTHERFN', have_another) + """ + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "HAVE_ANOTHERFN": True, + }, + ) + + def test_try_compile_msg(self): + cmd = textwrap.dedent( + """\ + known_flag = try_compile(language='C++', flags=['-fknown-flag'], + check_msg='whether -fknown-flag works') + set_config('HAVE_KNOWN_FLAG', known_flag) + """ + ) + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual(config, {"HAVE_KNOWN_FLAG": True}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking whether -fknown-flag works... yes + """ + ), + ) + + def test_check_header(self): + expected_test_content = textwrap.dedent( + """\ + #include <foo.h> + int + main(void) + { + + ; + return 0; + } + """ + ) + + cmd = textwrap.dedent( + """\ + check_header('foo.h') + """ + ) + + config, out, status = self.do_compile_test( + cmd, expected_test_content=expected_test_content + ) + self.assertEqual(status, 0) + self.assertEqual(config, {"DEFINES": {"HAVE_FOO_H": True}}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo.h... yes + """ + ), + ) + + def test_check_header_conditional(self): + cmd = textwrap.dedent( + """\ + check_headers('foo.h', 'bar.h', when=never) + """ + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual(out, "") + self.assertEqual(config, {"DEFINES": {}}) + + def test_check_header_include(self): + expected_test_content = textwrap.dedent( + """\ + #include <std.h> + #include <bar.h> + #include <foo.h> + int + main(void) + { + + ; + return 0; + } + """ + ) + + cmd = textwrap.dedent( + """\ + have_foo = check_header('foo.h', includes=['std.h', 'bar.h']) + set_config('HAVE_FOO_H', have_foo) + """ + ) + + config, out, status = self.do_compile_test( + cmd, expected_test_content=expected_test_content + ) + + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "HAVE_FOO_H": True, + "DEFINES": { + "HAVE_FOO_H": True, + }, + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for foo.h... yes + """ + ), + ) + + def test_check_headers_multiple(self): + cmd = textwrap.dedent( + """\ + baz_bar, quux_bar = check_headers('baz/foo-bar.h', 'baz-quux/foo-bar.h') + set_config('HAVE_BAZ_BAR', baz_bar) + set_config('HAVE_QUUX_BAR', quux_bar) + """ + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "HAVE_BAZ_BAR": True, + "HAVE_QUUX_BAR": True, + "DEFINES": { + "HAVE_BAZ_FOO_BAR_H": True, + "HAVE_BAZ_QUUX_FOO_BAR_H": True, + }, + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for baz/foo-bar.h... yes + checking for baz-quux/foo-bar.h... yes + """ + ), + ) + + def test_check_headers_not_found(self): + + cmd = textwrap.dedent( + """\ + baz_bar, quux_bar = check_headers('baz/foo-bar.h', 'baz-quux/foo-bar.h', + flags=['-funknown-flag']) + set_config('HAVE_BAZ_BAR', baz_bar) + set_config('HAVE_QUUX_BAR', quux_bar) + """ + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual(config, {"DEFINES": {}}) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking for baz/foo-bar.h... no + checking for baz-quux/foo-bar.h... no + """ + ), + ) + + +class TestWarningChecks(BaseCompileChecks): + def get_warnings(self): + return textwrap.dedent( + """\ + set_config('_WARNINGS_CFLAGS', warnings_flags.cflags) + set_config('_WARNINGS_CXXFLAGS', warnings_flags.cxxflags) + """ + ) + + def test_check_and_add_warning(self): + for flag, expected_flags in ( + ("-Wfoo", ["-Werror", "-Wfoo"]), + ("-Wno-foo", ["-Werror", "-Wfoo"]), + ("-Werror=foo", ["-Werror=foo"]), + ("-Wno-error=foo", ["-Wno-error=foo"]), + ): + cmd = ( + textwrap.dedent( + """\ + check_and_add_warning('%s') + """ + % flag + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test( + cmd, expected_flags=expected_flags + ) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [flag], + "_WARNINGS_CXXFLAGS": [flag], + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking whether the C compiler supports {flag}... yes + checking whether the C++ compiler supports {flag}... yes + """.format( + flag=flag + ) + ), + ) + + def test_check_and_add_warning_one(self): + cmd = ( + textwrap.dedent( + """\ + check_and_add_warning('-Wfoo', cxx_compiler) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [], + "_WARNINGS_CXXFLAGS": ["-Wfoo"], + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking whether the C++ compiler supports -Wfoo... yes + """ + ), + ) + + def test_check_and_add_warning_when(self): + cmd = ( + textwrap.dedent( + """\ + @depends(when=True) + def never(): + return False + check_and_add_warning('-Wfoo', cxx_compiler, when=never) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [], + "_WARNINGS_CXXFLAGS": [], + }, + ) + self.assertEqual(out, "") + + cmd = ( + textwrap.dedent( + """\ + @depends(when=True) + def always(): + return True + check_and_add_warning('-Wfoo', cxx_compiler, when=always) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [], + "_WARNINGS_CXXFLAGS": ["-Wfoo"], + }, + ) + self.assertEqual( + out, + textwrap.dedent( + """\ + checking whether the C++ compiler supports -Wfoo... yes + """ + ), + ) + + def test_add_warning(self): + cmd = ( + textwrap.dedent( + """\ + add_warning('-Wfoo') + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": ["-Wfoo"], + "_WARNINGS_CXXFLAGS": ["-Wfoo"], + }, + ) + self.assertEqual(out, "") + + def test_add_warning_one(self): + cmd = ( + textwrap.dedent( + """\ + add_warning('-Wfoo', c_compiler) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": ["-Wfoo"], + "_WARNINGS_CXXFLAGS": [], + }, + ) + self.assertEqual(out, "") + + def test_add_warning_when(self): + cmd = ( + textwrap.dedent( + """\ + @depends(when=True) + def never(): + return False + add_warning('-Wfoo', c_compiler, when=never) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": [], + "_WARNINGS_CXXFLAGS": [], + }, + ) + self.assertEqual(out, "") + + cmd = ( + textwrap.dedent( + """\ + @depends(when=True) + def always(): + return True + add_warning('-Wfoo', c_compiler, when=always) + """ + ) + + self.get_warnings() + ) + + config, out, status = self.do_compile_test(cmd) + self.assertEqual(status, 0) + self.assertEqual( + config, + { + "_WARNINGS_CFLAGS": ["-Wfoo"], + "_WARNINGS_CXXFLAGS": [], + }, + ) + self.assertEqual(out, "") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_configure.py b/python/mozbuild/mozbuild/test/configure/test_configure.py new file mode 100644 index 0000000000..a5e42faae3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_configure.py @@ -0,0 +1,1986 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +import textwrap +import unittest + +import mozpack.path as mozpath +import six +from mozunit import MockedOpen, main +from six import StringIO + +from mozbuild.configure import ConfigureError, ConfigureSandbox +from mozbuild.configure.options import ( + InvalidOptionError, + NegativeOptionValue, + PositiveOptionValue, +) +from mozbuild.util import ReadOnlyNamespace, exec_, memoized_property + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +class TestConfigure(unittest.TestCase): + def get_config( + self, options=[], env={}, configure="moz.configure", prog="/bin/configure" + ): + config = {} + out = StringIO() + sandbox = ConfigureSandbox(config, env, [prog] + options, out, out) + + sandbox.run(mozpath.join(test_data_path, configure)) + + if "--help" in options: + return six.ensure_text(out.getvalue()), config + self.assertEqual("", out.getvalue()) + return config + + def moz_configure(self, source): + return MockedOpen( + {os.path.join(test_data_path, "moz.configure"): textwrap.dedent(source)} + ) + + def test_defaults(self): + config = self.get_config() + self.maxDiff = None + self.assertEqual( + { + "CHOICES": NegativeOptionValue(), + "DEFAULTED": PositiveOptionValue(("not-simple",)), + "IS_GCC": NegativeOptionValue(), + "REMAINDER": ( + PositiveOptionValue(), + NegativeOptionValue(), + NegativeOptionValue(), + NegativeOptionValue(), + ), + "SIMPLE": NegativeOptionValue(), + "VALUES": NegativeOptionValue(), + "VALUES2": NegativeOptionValue(), + "VALUES3": NegativeOptionValue(), + "WITH_ENV": NegativeOptionValue(), + }, + config, + ) + + def test_help(self): + help, config = self.get_config(["--help"], prog="configure") + + self.assertEqual({}, config) + self.maxDiff = None + self.assertEqual( + "Usage: configure [options]\n" + "\n" + "Options: [defaults in brackets after descriptions]\n" + " Help options:\n" + " --help print this message\n" + "\n" + " Options from python/mozbuild/mozbuild/test/configure/data/included.configure:\n" + " --enable-imports-in-template\n Imports in template\n" + "\n" + " Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n" + " --enable-include Include\n" + " --enable-simple Enable simple\n" + " --enable-values Enable values\n" + " --enable-with-env Enable with env\n" + " --indirect-option Indirectly defined option\n" + " --option Option\n" + " --returned-choices Choices\n" + " --with-imports Imports\n" + " --with-returned-default Returned default [not-simple]\n" + " --with-stuff Build with stuff\n" + " --without-thing Build without thing\n" + "\n" + "\n" + "Environment variables:\n" + " Options from python/mozbuild/mozbuild/test/configure/data/moz.configure:\n" + " CC C Compiler\n" + "\n", + help.replace("\\", "/"), + ) + + def test_unknown(self): + with self.assertRaises(InvalidOptionError): + self.get_config(["--unknown"]) + + def test_simple(self): + for config in ( + self.get_config(), + self.get_config(["--disable-simple"]), + # Last option wins. + self.get_config(["--enable-simple", "--disable-simple"]), + ): + self.assertNotIn("ENABLED_SIMPLE", config) + self.assertIn("SIMPLE", config) + self.assertEqual(NegativeOptionValue(), config["SIMPLE"]) + + for config in ( + self.get_config(["--enable-simple"]), + self.get_config(["--disable-simple", "--enable-simple"]), + ): + self.assertIn("ENABLED_SIMPLE", config) + self.assertIn("SIMPLE", config) + self.assertEqual(PositiveOptionValue(), config["SIMPLE"]) + self.assertIs(config["SIMPLE"], config["ENABLED_SIMPLE"]) + + # --enable-simple doesn't take values. + with self.assertRaises(InvalidOptionError): + self.get_config(["--enable-simple=value"]) + + def test_with_env(self): + for config in ( + self.get_config(), + self.get_config(["--disable-with-env"]), + self.get_config(["--enable-with-env", "--disable-with-env"]), + self.get_config(env={"MOZ_WITH_ENV": ""}), + # Options win over environment + self.get_config(["--disable-with-env"], env={"MOZ_WITH_ENV": "1"}), + ): + self.assertIn("WITH_ENV", config) + self.assertEqual(NegativeOptionValue(), config["WITH_ENV"]) + + for config in ( + self.get_config(["--enable-with-env"]), + self.get_config(["--disable-with-env", "--enable-with-env"]), + self.get_config(env={"MOZ_WITH_ENV": "1"}), + self.get_config(["--enable-with-env"], env={"MOZ_WITH_ENV": ""}), + ): + self.assertIn("WITH_ENV", config) + self.assertEqual(PositiveOptionValue(), config["WITH_ENV"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(["--enable-with-env=value"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(env={"MOZ_WITH_ENV": "value"}) + + def test_values(self, name="VALUES"): + for config in ( + self.get_config(), + self.get_config(["--disable-values"]), + self.get_config(["--enable-values", "--disable-values"]), + ): + self.assertIn(name, config) + self.assertEqual(NegativeOptionValue(), config[name]) + + for config in ( + self.get_config(["--enable-values"]), + self.get_config(["--disable-values", "--enable-values"]), + ): + self.assertIn(name, config) + self.assertEqual(PositiveOptionValue(), config[name]) + + config = self.get_config(["--enable-values=foo"]) + self.assertIn(name, config) + self.assertEqual(PositiveOptionValue(("foo",)), config[name]) + + config = self.get_config(["--enable-values=foo,bar"]) + self.assertIn(name, config) + self.assertTrue(config[name]) + self.assertEqual(PositiveOptionValue(("foo", "bar")), config[name]) + + def test_values2(self): + self.test_values("VALUES2") + + def test_values3(self): + self.test_values("VALUES3") + + def test_returned_default(self): + config = self.get_config(["--enable-simple"]) + self.assertIn("DEFAULTED", config) + self.assertEqual(PositiveOptionValue(("simple",)), config["DEFAULTED"]) + + config = self.get_config(["--disable-simple"]) + self.assertIn("DEFAULTED", config) + self.assertEqual(PositiveOptionValue(("not-simple",)), config["DEFAULTED"]) + + def test_returned_choices(self): + for val in ("a", "b", "c"): + config = self.get_config( + ["--enable-values=alpha", "--returned-choices=%s" % val] + ) + self.assertIn("CHOICES", config) + self.assertEqual(PositiveOptionValue((val,)), config["CHOICES"]) + + for val in ("0", "1", "2"): + config = self.get_config( + ["--enable-values=numeric", "--returned-choices=%s" % val] + ) + self.assertIn("CHOICES", config) + self.assertEqual(PositiveOptionValue((val,)), config["CHOICES"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(["--enable-values=numeric", "--returned-choices=a"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(["--enable-values=alpha", "--returned-choices=0"]) + + def test_included(self): + config = self.get_config(env={"CC": "gcc"}) + self.assertIn("IS_GCC", config) + self.assertEqual(config["IS_GCC"], True) + + config = self.get_config(["--enable-include=extra.configure", "--extra"]) + self.assertIn("EXTRA", config) + self.assertEqual(PositiveOptionValue(), config["EXTRA"]) + + with self.assertRaises(InvalidOptionError): + self.get_config(["--extra"]) + + def test_template(self): + config = self.get_config(env={"CC": "gcc"}) + self.assertIn("CFLAGS", config) + self.assertEqual(config["CFLAGS"], ["-Werror=foobar"]) + + config = self.get_config(env={"CC": "clang"}) + self.assertNotIn("CFLAGS", config) + + def test_imports(self): + config = {} + out = StringIO() + sandbox = ConfigureSandbox(config, {}, ["configure"], out, out) + + with self.assertRaises(ImportError): + exec_( + textwrap.dedent( + """ + @template + def foo(): + import sys + foo()""" + ), + sandbox, + ) + + exec_( + textwrap.dedent( + """ + @template + @imports('sys') + def foo(): + return sys""" + ), + sandbox, + ) + + self.assertIs(sandbox["foo"](), sys) + + # os.path after an import is a mix of vanilla os.path and sandbox os.path. + os_path = {} + exec_("from os.path import *", {}, os_path) + os_path.update(sandbox.OS.path.__dict__) + os_path = ReadOnlyNamespace(**os_path) + + exec_( + textwrap.dedent( + """ + @template + @imports(_from='os', _import='path') + def foo(): + return path""" + ), + sandbox, + ) + + self.assertEqual(sandbox["foo"](), os_path) + + exec_( + textwrap.dedent( + """ + @template + @imports(_from='os', _import='path', _as='os_path') + def foo(): + return os_path""" + ), + sandbox, + ) + + self.assertEqual(sandbox["foo"](), os_path) + + exec_( + textwrap.dedent( + """ + @template + @imports('__builtin__') + def foo(): + return __builtin__""" + ), + sandbox, + ) + + self.assertIs(sandbox["foo"](), six.moves.builtins) + + exec_( + textwrap.dedent( + """ + @template + @imports(_from='__builtin__', _import='open') + def foo(): + return open('%s')""" + % os.devnull + ), + sandbox, + ) + + f = sandbox["foo"]() + self.assertEqual(f.name, os.devnull) + f.close() + + # This unlocks the sandbox + exec_( + textwrap.dedent( + """ + @template + @imports(_import='__builtin__', _as='__builtins__') + def foo(): + import sys + return sys""" + ), + sandbox, + ) + + self.assertIs(sandbox["foo"](), sys) + + exec_( + textwrap.dedent( + """ + @template + @imports('__sandbox__') + def foo(): + return __sandbox__""" + ), + sandbox, + ) + + self.assertIs(sandbox["foo"](), sandbox) + + exec_( + textwrap.dedent( + """ + @template + @imports(_import='__sandbox__', _as='s') + def foo(): + return s""" + ), + sandbox, + ) + + self.assertIs(sandbox["foo"](), sandbox) + + # Nothing leaked from the function being executed + self.assertEqual(list(sandbox), ["__builtins__", "foo"]) + self.assertEqual(sandbox["__builtins__"], ConfigureSandbox.BUILTINS) + + exec_( + textwrap.dedent( + """ + @template + @imports('sys') + def foo(): + @depends(when=True) + def bar(): + return sys + return bar + bar = foo()""" + ), + sandbox, + ) + + with self.assertRaises(NameError) as e: + sandbox._depends[sandbox["bar"]].result() + + self.assertIn("name 'sys' is not defined", str(e.exception)) + + def test_apply_imports(self): + imports = [] + + class CountApplyImportsSandbox(ConfigureSandbox): + def _apply_imports(self, *args, **kwargs): + imports.append((args, kwargs)) + super(CountApplyImportsSandbox, self)._apply_imports(*args, **kwargs) + + config = {} + out = StringIO() + sandbox = CountApplyImportsSandbox(config, {}, ["configure"], out, out) + + exec_( + textwrap.dedent( + """ + @template + @imports('sys') + def foo(): + return sys + foo() + foo()""" + ), + sandbox, + ) + + self.assertEqual(len(imports), 1) + + def test_import_wrapping(self): + bar = object() + foo = ReadOnlyNamespace(bar=bar) + + class BasicWrappingSandbox(ConfigureSandbox): + @memoized_property + def _wrapped_foo(self): + return foo + + config = {} + out = StringIO() + sandbox = BasicWrappingSandbox(config, {}, ["configure"], out, out) + + exec_( + textwrap.dedent( + """ + @template + @imports('foo') + def toplevel(): + return foo + @template + @imports('foo.bar') + def bar(): + return foo.bar + @template + @imports('foo.bar') + def bar_upper(): + return foo + @template + @imports(_from='foo', _import='bar') + def from_import(): + return bar + @template + @imports(_from='foo', _import='bar', _as='custom_name') + def from_import_as(): + return custom_name + @template + @imports(_import='foo', _as='custom_name') + def import_as(): + return custom_name + """ + ), + sandbox, + ) + self.assertIs(sandbox["toplevel"](), foo) + self.assertIs(sandbox["bar"](), bar) + self.assertIs(sandbox["bar_upper"](), foo) + self.assertIs(sandbox["from_import"](), bar) + self.assertIs(sandbox["from_import_as"](), bar) + self.assertIs(sandbox["import_as"](), foo) + + def test_os_path(self): + config = self.get_config(["--with-imports=%s" % __file__]) + self.assertIn("HAS_ABSPATH", config) + self.assertEqual(config["HAS_ABSPATH"], True) + self.assertIn("HAS_GETATIME", config) + self.assertEqual(config["HAS_GETATIME"], True) + self.assertIn("HAS_GETATIME2", config) + self.assertEqual(config["HAS_GETATIME2"], False) + + def test_template_call(self): + config = self.get_config(env={"CC": "gcc"}) + self.assertIn("TEMPLATE_VALUE", config) + self.assertEqual(config["TEMPLATE_VALUE"], 42) + self.assertIn("TEMPLATE_VALUE_2", config) + self.assertEqual(config["TEMPLATE_VALUE_2"], 21) + + def test_template_imports(self): + config = self.get_config(["--enable-imports-in-template"]) + self.assertIn("PLATFORM", config) + self.assertEqual(config["PLATFORM"], sys.platform) + + def test_decorators(self): + config = {} + out = StringIO() + sandbox = ConfigureSandbox(config, {}, ["configure"], out, out) + + sandbox.include_file(mozpath.join(test_data_path, "decorators.configure")) + + self.assertNotIn("FOO", sandbox) + self.assertNotIn("BAR", sandbox) + self.assertNotIn("QUX", sandbox) + + def test_set_config(self): + def get_config(*args): + return self.get_config(*args, configure="set_config.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config(["--set-foo"]) + self.assertIn("FOO", config) + self.assertEqual(config["FOO"], True) + + config = get_config(["--set-bar"]) + self.assertNotIn("FOO", config) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], True) + + config = get_config(["--set-value=qux"]) + self.assertIn("VALUE", config) + self.assertEqual(config["VALUE"], "qux") + + config = get_config(["--set-name=hoge"]) + self.assertIn("hoge", config) + self.assertEqual(config["hoge"], True) + + config = get_config([]) + self.assertEqual(config, {"BAR": False}) + + with self.assertRaises(ConfigureError): + # Both --set-foo and --set-name=FOO are going to try to + # set_config('FOO'...) + get_config(["--set-foo", "--set-name=FOO"]) + + def test_set_config_when(self): + with self.moz_configure( + """ + option('--with-qux', help='qux') + set_config('FOO', 'foo', when=True) + set_config('BAR', 'bar', when=False) + set_config('QUX', 'qux', when='--with-qux') + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": "foo", + }, + ) + config = self.get_config(["--with-qux"]) + self.assertEqual( + config, + { + "FOO": "foo", + "QUX": "qux", + }, + ) + + def test_set_config_when_disable(self): + with self.moz_configure( + """ + option('--disable-baz', help='Disable baz') + set_config('BAZ', True, when='--enable-baz') + """ + ): + config = self.get_config() + self.assertEqual(config["BAZ"], True) + config = self.get_config(["--enable-baz"]) + self.assertEqual(config["BAZ"], True) + config = self.get_config(["--disable-baz"]) + self.assertEqual(config, {}) + + def test_set_define(self): + def get_config(*args): + return self.get_config(*args, configure="set_define.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {"DEFINES": {}}) + + config = get_config(["--set-foo"]) + self.assertIn("FOO", config["DEFINES"]) + self.assertEqual(config["DEFINES"]["FOO"], True) + + config = get_config(["--set-bar"]) + self.assertNotIn("FOO", config["DEFINES"]) + self.assertIn("BAR", config["DEFINES"]) + self.assertEqual(config["DEFINES"]["BAR"], True) + + config = get_config(["--set-value=qux"]) + self.assertIn("VALUE", config["DEFINES"]) + self.assertEqual(config["DEFINES"]["VALUE"], "qux") + + config = get_config(["--set-name=hoge"]) + self.assertIn("hoge", config["DEFINES"]) + self.assertEqual(config["DEFINES"]["hoge"], True) + + config = get_config([]) + self.assertEqual(config["DEFINES"], {"BAR": False}) + + with self.assertRaises(ConfigureError): + # Both --set-foo and --set-name=FOO are going to try to + # set_define('FOO'...) + get_config(["--set-foo", "--set-name=FOO"]) + + def test_set_define_when(self): + with self.moz_configure( + """ + option('--with-qux', help='qux') + set_define('FOO', 'foo', when=True) + set_define('BAR', 'bar', when=False) + set_define('QUX', 'qux', when='--with-qux') + """ + ): + config = self.get_config() + self.assertEqual( + config["DEFINES"], + { + "FOO": "foo", + }, + ) + config = self.get_config(["--with-qux"]) + self.assertEqual( + config["DEFINES"], + { + "FOO": "foo", + "QUX": "qux", + }, + ) + + def test_set_define_when_disable(self): + with self.moz_configure( + """ + option('--disable-baz', help='Disable baz') + set_define('BAZ', True, when='--enable-baz') + """ + ): + config = self.get_config() + self.assertEqual(config["DEFINES"]["BAZ"], True) + config = self.get_config(["--enable-baz"]) + self.assertEqual(config["DEFINES"]["BAZ"], True) + config = self.get_config(["--disable-baz"]) + self.assertEqual(config["DEFINES"], {}) + + def test_imply_option_simple(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/simple.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + config = get_config(["--enable-foo"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], PositiveOptionValue()) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--enable-foo", "--disable-bar"]) + + self.assertEqual( + str(e.exception), + "'--enable-bar' implied by '--enable-foo' conflicts with " + "'--disable-bar' from the command-line", + ) + + def test_imply_option_negative(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/negative.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + config = get_config(["--enable-foo"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], NegativeOptionValue()) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--enable-foo", "--enable-bar"]) + + self.assertEqual( + str(e.exception), + "'--disable-bar' implied by '--enable-foo' conflicts with " + "'--enable-bar' from the command-line", + ) + + config = get_config(["--disable-hoge"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], NegativeOptionValue()) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--disable-hoge", "--enable-bar"]) + + self.assertEqual( + str(e.exception), + "'--disable-bar' implied by '--disable-hoge' conflicts with " + "'--enable-bar' from the command-line", + ) + + def test_imply_option_values(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/values.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + config = get_config(["--enable-foo=a"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], PositiveOptionValue(("a",))) + + config = get_config(["--enable-foo=a,b"]) + self.assertIn("BAR", config) + self.assertEqual(config["BAR"], PositiveOptionValue(("a", "b"))) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--enable-foo=a,b", "--disable-bar"]) + + self.assertEqual( + str(e.exception), + "'--enable-bar=a,b' implied by '--enable-foo' conflicts with " + "'--disable-bar' from the command-line", + ) + + def test_imply_option_infer(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/infer.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + with self.assertRaises(InvalidOptionError) as e: + get_config(["--enable-foo", "--disable-bar"]) + + self.assertEqual( + str(e.exception), + "'--enable-bar' implied by '--enable-foo' conflicts with " + "'--disable-bar' from the command-line", + ) + + with self.assertRaises(ConfigureError) as e: + self.get_config([], configure="imply_option/infer_ko.configure") + + self.assertEqual( + str(e.exception), + "Cannot infer what implies '--enable-bar'. Please add a `reason` " + "to the `imply_option` call.", + ) + + def test_imply_option_immediate_value(self): + def get_config(*args): + return self.get_config(*args, configure="imply_option/imm.configure") + + help, config = get_config(["--help"]) + self.assertEqual(config, {}) + + config = get_config([]) + self.assertEqual(config, {}) + + config_path = mozpath.abspath( + mozpath.join(test_data_path, "imply_option", "imm.configure") + ) + + with self.assertRaisesRegexp( + InvalidOptionError, + "--enable-foo' implied by 'imply_option at %s:7' conflicts " + "with '--disable-foo' from the command-line" % config_path, + ): + get_config(["--disable-foo"]) + + with self.assertRaisesRegexp( + InvalidOptionError, + "--enable-bar=foo,bar' implied by 'imply_option at %s:18' " + "conflicts with '--enable-bar=a,b,c' from the command-line" % config_path, + ): + get_config(["--enable-bar=a,b,c"]) + + with self.assertRaisesRegexp( + InvalidOptionError, + "--enable-baz=BAZ' implied by 'imply_option at %s:29' " + "conflicts with '--enable-baz=QUUX' from the command-line" % config_path, + ): + get_config(["--enable-baz=QUUX"]) + + def test_imply_option_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + imply_option('--with-foo', ('a',), 'bar') + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), + "`--with-foo`, emitted from `%s` line 2, is unknown." + % mozpath.join(test_data_path, "moz.configure"), + ) + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + imply_option('--with-foo', 42, 'bar') + + option('--with-foo', help='foo') + @depends('--with-foo') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Unexpected type: 'int'") + + def test_imply_option_when(self): + with self.moz_configure( + """ + option('--with-foo', help='foo') + imply_option('--with-qux', True, when='--with-foo') + option('--with-qux', help='qux') + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo"]) + self.assertEqual( + config, + { + "QUX": PositiveOptionValue(), + }, + ) + + def test_imply_option_dependency_loop(self): + with self.moz_configure( + """ + option('--without-foo', help='foo') + + @depends('--with-foo') + def qux_default(foo): + return bool(foo) + + option('--with-qux', default=qux_default, help='qux') + + imply_option('--with-foo', depends('--with-qux')(lambda x: x or None)) + + set_config('FOO', depends('--with-foo')(lambda x: x)) + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + config = self.get_config(["--without-foo"]) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-qux"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--without-foo", "--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by '--with-qux' conflicts " + "with '--without-foo' from the command-line", + ) + + config = self.get_config(["--without-qux"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + with self.moz_configure( + """ + option('--with-foo', help='foo') + + @depends('--with-foo') + def qux_default(foo): + return bool(foo) + + option('--with-qux', default=qux_default, help='qux') + + imply_option('--with-foo', depends('--with-qux')(lambda x: x or None)) + + set_config('FOO', depends('--with-foo')(lambda x: x)) + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by '--with-qux' conflicts " + "with '--without-foo' from the default", + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--without-foo", "--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by '--with-qux' conflicts " + "with '--without-foo' from the command-line", + ) + + config = self.get_config(["--without-qux"]) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure")) + + # Same test as above, but using `when` in the `imply_option`. + with self.moz_configure( + """ + option('--with-foo', help='foo') + + @depends('--with-foo') + def qux_default(foo): + return bool(foo) + + option('--with-qux', default=qux_default, help='qux') + + imply_option('--with-foo', True, when='--with-qux') + + set_config('FOO', depends('--with-foo')(lambda x: x)) + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by 'imply_option at %s:10' conflicts " + "with '--without-foo' from the default" % config_path, + ) + + with self.assertRaises(InvalidOptionError) as e: + config = self.get_config(["--without-foo", "--with-qux"]) + + self.assertEqual( + str(e.exception), + "'--with-foo' implied by 'imply_option at %s:10' conflicts " + "with '--without-foo' from the command-line" % config_path, + ) + + config = self.get_config(["--without-qux"]) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + def test_imply_option_recursion(self): + config_path = mozpath.abspath(mozpath.join(test_data_path, "moz.configure")) + + message = ( + "'--without-foo' appears somewhere in the direct or indirect dependencies " + "when resolving imply_option at %s:8" % config_path + ) + + with self.moz_configure( + """ + option('--without-foo', help='foo') + + imply_option('--with-qux', depends('--with-foo')(lambda x: x or None)) + + option('--with-qux', help='qux') + + imply_option('--with-foo', depends('--with-qux')(lambda x: x or None)) + + set_config('FOO', depends('--with-foo')(lambda x: x)) + set_config('QUX', depends('--with-qux')(lambda x: x)) + """ + ): + # Note: no error is detected when the depends function in the + # imply_options resolve to None, which disables the imply_option. + + with self.assertRaises(ConfigureError) as e: + self.get_config() + + self.assertEqual(str(e.exception), message) + + with self.assertRaises(ConfigureError) as e: + self.get_config(["--with-qux"]) + + self.assertEqual(str(e.exception), message) + + with self.assertRaises(ConfigureError) as e: + self.get_config(["--without-foo", "--with-qux"]) + + self.assertEqual(str(e.exception), message) + + def test_option_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure('option("--with-foo", help="foo")'): + self.get_config() + + self.assertEqual( + str(e.exception), + "Option `--with-foo` is not handled ; reference it with a @depends", + ) + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option("--with-foo", help="foo") + option("--with-foo", help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `--with-foo` already defined") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option(env="MOZ_FOO", help="foo") + option(env="MOZ_FOO", help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option('--with-foo', env="MOZ_FOO", help="foo") + option(env="MOZ_FOO", help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option(env="MOZ_FOO", help="foo") + option('--with-foo', env="MOZ_FOO", help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `MOZ_FOO` already defined") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option('--with-foo', env="MOZ_FOO", help="foo") + option('--with-foo', help="foo") + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option `--with-foo` already defined") + + def test_option_when(self): + with self.moz_configure( + """ + option('--with-foo', help='foo', when=True) + option('--with-bar', help='bar', when=False) + option('--with-qux', env="QUX", help='qux', when='--with-foo') + + set_config('FOO', depends('--with-foo', when=True)(lambda x: x)) + set_config('BAR', depends('--with-bar', when=False)(lambda x: x)) + set_config('QUX', depends('--with-qux', when='--with-foo')(lambda x: x)) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--with-foo", "--with-qux"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(), + "QUX": PositiveOptionValue(), + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-bar"]) + + self.assertEqual( + str(e.exception), "--with-bar is not available in this configuration" + ) + + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-qux"]) + + self.assertEqual( + str(e.exception), "--with-qux is not available in this configuration" + ) + + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["QUX=1"]) + + self.assertEqual( + str(e.exception), "QUX is not available in this configuration" + ) + + config = self.get_config(env={"QUX": "1"}) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + }, + ) + + help, config = self.get_config(["--help"]) + self.assertEqual( + help.replace("\\", "/"), + textwrap.dedent( + """\ + Usage: configure [options] + + Options: [defaults in brackets after descriptions] + Help options: + --help print this message + + Options from python/mozbuild/mozbuild/test/configure/data/moz.configure: + --with-foo foo + + + Environment variables: + """ + ), + ) + + help, config = self.get_config(["--help", "--with-foo"]) + self.assertEqual( + help.replace("\\", "/"), + textwrap.dedent( + """\ + Usage: configure [options] + + Options: [defaults in brackets after descriptions] + Help options: + --help print this message + + Options from python/mozbuild/mozbuild/test/configure/data/moz.configure: + --with-foo foo + --with-qux qux + + + Environment variables: + """ + ), + ) + + with self.moz_configure( + """ + option('--with-foo', help='foo', when=True) + set_config('FOO', depends('--with-foo')(lambda x: x)) + """ + ): + with self.assertRaises(ConfigureError) as e: + self.get_config() + + self.assertEqual( + str(e.exception), + "@depends function needs the same `when` as " "options it depends on", + ) + + with self.moz_configure( + """ + @depends(when=True) + def always(): + return True + @depends(when=True) + def always2(): + return True + option('--with-foo', help='foo', when=always) + set_config('FOO', depends('--with-foo', when=always2)(lambda x: x)) + """ + ): + with self.assertRaises(ConfigureError) as e: + self.get_config() + + self.assertEqual( + str(e.exception), + "@depends function needs the same `when` as " "options it depends on", + ) + + with self.moz_configure( + """ + @depends(when=True) + def always(): + return True + @depends(when=True) + def always2(): + return True + with only_when(always2): + option('--with-foo', help='foo', when=always) + # include() triggers resolution of its dependencies, and their + # side effects. + include(depends('--with-foo', when=always)(lambda x: x)) + # The sandbox should figure that the `when` here is + # appropriate. Bad behavior in CombinedDependsFunction.__eq__ + # made this fail in the past. + set_config('FOO', depends('--with-foo', when=always)(lambda x: x)) + """ + ): + self.get_config() + + with self.moz_configure( + """ + option('--with-foo', help='foo') + option('--without-bar', help='bar', when='--with-foo') + option('--with-qux', help='qux', when='--with-bar') + set_config('QUX', True, when='--with-qux') + """ + ): + # These are valid: + self.get_config(["--with-foo"]) + self.get_config(["--with-foo", "--with-bar"]) + self.get_config(["--with-foo", "--without-bar"]) + self.get_config(["--with-foo", "--with-bar", "--with-qux"]) + self.get_config(["--with-foo", "--with-bar", "--without-qux"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-bar"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--without-bar"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-qux"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--without-qux"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-foo", "--without-bar", "--with-qux"]) + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-foo", "--without-bar", "--without-qux"]) + + def test_include_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure('include("../foo.configure")'): + self.get_config() + + self.assertEqual( + str(e.exception), + "Cannot include `%s` because it is not in a subdirectory of `%s`" + % ( + mozpath.normpath(mozpath.join(test_data_path, "..", "foo.configure")), + mozpath.normsep(test_data_path), + ), + ) + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + include('extra.configure') + include('extra.configure') + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), + "Cannot include `%s` because it was included already." + % mozpath.normpath(mozpath.join(test_data_path, "extra.configure")), + ) + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + include(42) + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Unexpected type: 'int'") + + def test_include_when(self): + with MockedOpen( + { + os.path.join(test_data_path, "moz.configure"): textwrap.dedent( + """ + option('--with-foo', help='foo') + + include('always.configure', when=True) + include('never.configure', when=False) + include('foo.configure', when='--with-foo') + + set_config('FOO', foo) + set_config('BAR', bar) + set_config('QUX', qux) + """ + ), + os.path.join(test_data_path, "always.configure"): textwrap.dedent( + """ + option('--with-bar', help='bar') + @depends('--with-bar') + def bar(x): + if x: + return 'bar' + """ + ), + os.path.join(test_data_path, "never.configure"): textwrap.dedent( + """ + option('--with-qux', help='qux') + @depends('--with-qux') + def qux(x): + if x: + return 'qux' + """ + ), + os.path.join(test_data_path, "foo.configure"): textwrap.dedent( + """ + option('--with-foo-really', help='really foo') + @depends('--with-foo-really') + def foo(x): + if x: + return 'foo' + + include('foo2.configure', when='--with-foo-really') + """ + ), + os.path.join(test_data_path, "foo2.configure"): textwrap.dedent( + """ + set_config('FOO2', True) + """ + ), + } + ): + config = self.get_config() + self.assertEqual(config, {}) + + config = self.get_config(["--with-foo"]) + self.assertEqual(config, {}) + + config = self.get_config(["--with-bar"]) + self.assertEqual( + config, + { + "BAR": "bar", + }, + ) + + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--with-qux"]) + + self.assertEqual( + str(e.exception), "--with-qux is not available in this configuration" + ) + + config = self.get_config(["--with-foo", "--with-foo-really"]) + self.assertEqual( + config, + { + "FOO": "foo", + "FOO2": True, + }, + ) + + def test_sandbox_failures(self): + with self.assertRaises(KeyError) as e: + with self.moz_configure( + """ + include = 42 + """ + ): + self.get_config() + + self.assertIn("Cannot reassign builtins", str(e.exception)) + + with self.assertRaises(KeyError) as e: + with self.moz_configure( + """ + foo = 42 + """ + ): + self.get_config() + + self.assertIn( + "Cannot assign `foo` because it is neither a @depends nor a " "@template", + str(e.exception), + ) + + def test_depends_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @depends() + def foo(): + return + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "@depends needs at least one argument") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @depends('--with-foo') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), + "'--with-foo' is not a known option. Maybe it's " "declared too late?", + ) + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @depends('--with-foo=42') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Option must not contain an '='") + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @depends(42) + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), + "Cannot use object of type 'int' as argument " "to @depends", + ) + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @depends('--help') + def foo(value): + yield + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), "Cannot decorate generator functions with @depends" + ) + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @depends('--help') + def foo(value): + return value + + depends('--help')(foo) + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Cannot nest @depends functions") + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @template + def foo(f): + pass + + depends('--help')(foo) + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Cannot use a @template function here") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return value + + foo() + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "The `foo` function may not be called") + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @depends('--help', foo=42) + def foo(_): + return + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), "depends_impl() got an unexpected keyword argument 'foo'" + ) + + def test_depends_when(self): + with self.moz_configure( + """ + @depends(when=True) + def foo(): + return 'foo' + + set_config('FOO', foo) + + @depends(when=False) + def bar(): + return 'bar' + + set_config('BAR', bar) + + option('--with-qux', help='qux') + @depends(when='--with-qux') + def qux(): + return 'qux' + + set_config('QUX', qux) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": "foo", + }, + ) + + config = self.get_config(["--with-qux"]) + self.assertEqual( + config, + { + "FOO": "foo", + "QUX": "qux", + }, + ) + + def test_depends_value(self): + with self.moz_configure( + """ + foo = depends(when=True)('foo') + + set_config('FOO', foo) + + bar = depends(when=False)('bar') + + set_config('BAR', bar) + + option('--with-qux', help='qux') + @depends(when='--with-qux') + def qux(): + return 'qux' + + set_config('QUX', qux) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": "foo", + }, + ) + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + + depends('--foo')('foo') + """ + ): + self.get_config() + + self.assertEqual( + str(e.exception), "Cannot wrap literal values in @depends with dependencies" + ) + + def test_imports_failures(self): + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + @imports('os') + @template + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "@imports must appear after @template") + + with self.assertRaises(ConfigureError) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @imports('os') + @depends('--foo') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "@imports must appear after @depends") + + for import_ in ( + "42", + "_from=42, _import='os'", + "_from='os', _import='path', _as=42", + ): + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @imports(%s) + @template + def foo(value): + return value + """ + % import_ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Unexpected type: 'int'") + + with self.assertRaises(TypeError) as e: + with self.moz_configure( + """ + @imports('os', 42) + @template + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Unexpected type: 'int'") + + with self.assertRaises(ValueError) as e: + with self.moz_configure( + """ + @imports('os*') + def foo(value): + return value + """ + ): + self.get_config() + + self.assertEqual(str(e.exception), "Invalid argument to @imports: 'os*'") + + def test_only_when(self): + moz_configure = """ + option('--enable-when', help='when') + @depends('--enable-when', '--help') + def when(value, _): + return bool(value) + + with only_when(when): + option('--foo', nargs='*', help='foo') + @depends('--foo') + def foo(value): + return value + + set_config('FOO', foo) + set_define('FOO', foo) + + # It is possible to depend on a function defined in a only_when + # block. It then resolves to `None`. + set_config('BAR', depends(foo)(lambda x: x)) + set_define('BAR', depends(foo)(lambda x: x)) + """ + + with self.moz_configure(moz_configure): + config = self.get_config() + self.assertEqual( + config, + { + "DEFINES": {}, + }, + ) + + config = self.get_config(["--enable-when"]) + self.assertEqual( + config, + { + "BAR": NegativeOptionValue(), + "FOO": NegativeOptionValue(), + "DEFINES": { + "BAR": NegativeOptionValue(), + "FOO": NegativeOptionValue(), + }, + }, + ) + + config = self.get_config(["--enable-when", "--foo=bar"]) + self.assertEqual( + config, + { + "BAR": PositiveOptionValue(["bar"]), + "FOO": PositiveOptionValue(["bar"]), + "DEFINES": { + "BAR": PositiveOptionValue(["bar"]), + "FOO": PositiveOptionValue(["bar"]), + }, + }, + ) + + # The --foo option doesn't exist when --enable-when is not given. + with self.assertRaises(InvalidOptionError) as e: + self.get_config(["--foo"]) + + self.assertEqual( + str(e.exception), "--foo is not available in this configuration" + ) + + # Cannot depend on an option defined in a only_when block, because we + # don't know what OptionValue would make sense. + with self.moz_configure( + moz_configure + + """ + set_config('QUX', depends('--foo')(lambda x: x)) + """ + ): + with self.assertRaises(ConfigureError) as e: + self.get_config() + + self.assertEqual( + str(e.exception), + "@depends function needs the same `when` as " "options it depends on", + ) + + with self.moz_configure( + moz_configure + + """ + set_config('QUX', depends('--foo', when=when)(lambda x: x)) + """ + ): + self.get_config(["--enable-when"]) + + # Using imply_option for an option defined in a only_when block fails + # similarly if the imply_option happens outside the block. + with self.moz_configure( + """ + imply_option('--foo', True) + """ + + moz_configure + ): + with self.assertRaises(InvalidOptionError) as e: + self.get_config() + + self.assertEqual( + str(e.exception), "--foo is not available in this configuration" + ) + + # And similarly doesn't fail when the condition is true. + with self.moz_configure( + """ + imply_option('--foo', True) + """ + + moz_configure + ): + self.get_config(["--enable-when"]) + + def test_depends_binary_ops(self): + with self.moz_configure( + """ + option('--foo', nargs=1, help='foo') + @depends('--foo') + def foo(value): + return value or 0 + + option('--bar', nargs=1, help='bar') + @depends('--bar') + def bar(value): + return value or '' + + option('--baz', nargs=1, help='baz') + @depends('--baz') + def baz(value): + return value + + set_config('FOOorBAR', foo | bar) + set_config('FOOorBARorBAZ', foo | bar | baz) + set_config('FOOandBAR', foo & bar) + set_config('FOOandBARandBAZ', foo & bar & baz) + """ + ): + for foo_opt, foo_value in ( + ("", 0), + ("--foo=foo", PositiveOptionValue(("foo",))), + ): + for bar_opt, bar_value in ( + ("", ""), + ("--bar=bar", PositiveOptionValue(("bar",))), + ): + for baz_opt, baz_value in ( + ("", NegativeOptionValue()), + ("--baz=baz", PositiveOptionValue(("baz",))), + ): + config = self.get_config( + [x for x in (foo_opt, bar_opt, baz_opt) if x] + ) + self.assertEqual( + config, + { + "FOOorBAR": foo_value or bar_value, + "FOOorBARorBAZ": foo_value or bar_value or baz_value, + "FOOandBAR": foo_value and bar_value, + "FOOandBARandBAZ": foo_value + and bar_value + and baz_value, + }, + ) + + def test_depends_getattr(self): + with self.moz_configure( + """ + @imports(_from='mozbuild.util', _import='ReadOnlyNamespace') + def namespace(**kwargs): + return ReadOnlyNamespace(**kwargs) + + option('--foo', nargs=1, help='foo') + @depends('--foo') + def foo(value): + return value + + option('--bar', nargs=1, help='bar') + @depends('--bar') + def bar(value): + return value or None + + @depends(foo, bar) + def foobar(foo, bar): + return namespace(foo=foo, bar=bar) + + set_config('FOO', foobar.foo) + set_config('BAR', foobar.bar) + set_config('BAZ', foobar.baz) + """ + ): + config = self.get_config() + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + }, + ) + + config = self.get_config(["--foo=foo"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(("foo",)), + }, + ) + + config = self.get_config(["--bar=bar"]) + self.assertEqual( + config, + { + "FOO": NegativeOptionValue(), + "BAR": PositiveOptionValue(("bar",)), + }, + ) + + config = self.get_config(["--foo=foo", "--bar=bar"]) + self.assertEqual( + config, + { + "FOO": PositiveOptionValue(("foo",)), + "BAR": PositiveOptionValue(("bar",)), + }, + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_lint.py b/python/mozbuild/mozbuild/test/configure/test_lint.py new file mode 100644 index 0000000000..7ecac769c3 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_lint.py @@ -0,0 +1,487 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import contextlib +import os +import sys +import textwrap +import traceback +import unittest + +import mozpack.path as mozpath +from mozunit import MockedOpen, main + +from mozbuild.configure import ConfigureError +from mozbuild.configure.lint import LintSandbox + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + + +class TestLint(unittest.TestCase): + def lint_test(self, options=[], env={}): + sandbox = LintSandbox(env, ["configure"] + options) + + sandbox.run(mozpath.join(test_data_path, "moz.configure")) + + def moz_configure(self, source): + return MockedOpen( + {os.path.join(test_data_path, "moz.configure"): textwrap.dedent(source)} + ) + + @contextlib.contextmanager + def assertRaisesFromLine(self, exc_type, line): + with self.assertRaises(exc_type) as e: + yield e + + _, _, tb = sys.exc_info() + self.assertEqual( + traceback.extract_tb(tb)[-1][:2], + (mozpath.join(test_data_path, "moz.configure"), line), + ) + + def test_configure_testcase(self): + # Lint python/mozbuild/mozbuild/test/configure/data/moz.configure + self.lint_test() + + def test_depends_failures(self): + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return value + + @depends('--help', foo) + @imports('os') + def bar(help, foo): + return foo + """ + ): + self.lint_test() + + with self.assertRaisesFromLine(ConfigureError, 7) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return value + + @depends('--help', foo) + def bar(help, foo): + return foo + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `--help` is unused") + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + @imports('os') + def foo(value): + return value + + @depends('--help', foo) + @imports('os') + def bar(help, foo): + return foo + """ + ): + self.lint_test() + + self.assertEqual( + str(e.exception), + "Missing '--help' dependency because `bar` depends on '--help' and `foo`", + ) + + with self.assertRaisesFromLine(ConfigureError, 7) as e: + with self.moz_configure( + """ + @template + def tmpl(): + qux = 42 + + option('--foo', help='foo') + @depends('--foo') + def foo(value): + qux + return value + + @depends('--help', foo) + @imports('os') + def bar(help, foo): + return foo + tmpl() + """ + ): + self.lint_test() + + self.assertEqual( + str(e.exception), + "Missing '--help' dependency because `bar` depends on '--help' and `foo`", + ) + + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return value + + include(foo) + """ + ): + self.lint_test() + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + @imports('os') + def foo(value): + return value + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "Missing '--help' dependency") + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + @imports('os') + def foo(value): + return value + + @depends(foo) + def bar(value): + return value + + include(bar) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "Missing '--help' dependency") + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + @imports('os') + def foo(value): + return value + + option('--bar', help='bar', when=foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "Missing '--help' dependency") + + # This would have failed with "Missing '--help' dependency" + # in the past, because of the reference to the builtin False. + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return False or value + + option('--bar', help='bar', when=foo) + """ + ): + self.lint_test() + + # However, when something that is normally a builtin is overridden, + # we should still want the dependency on --help. + with self.assertRaisesFromLine(ConfigureError, 7) as e: + with self.moz_configure( + """ + @template + def tmpl(): + sorted = 42 + + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return sorted + + option('--bar', help='bar', when=foo) + tmpl() + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "Missing '--help' dependency") + + # There is a default restricted `os` module when there is no explicit + # @imports, and it's fine to use it without a dependency on --help. + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + os + return value + + include(foo) + """ + ): + self.lint_test() + + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option('--foo', help='foo') + @depends('--foo') + def foo(value): + return + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `--foo` is unused") + + with self.assertRaisesFromLine(ConfigureError, 5) as e: + with self.moz_configure( + """ + @depends(when=True) + def bar(): + return + @depends(bar) + def foo(value): + return + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `bar` is unused") + + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + @depends(depends(when=True)(lambda: None)) + def foo(value): + return + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `<lambda>` is unused") + + with self.assertRaisesFromLine(ConfigureError, 9) as e: + with self.moz_configure( + """ + @template + def tmpl(): + @depends(when=True) + def bar(): + return + return bar + qux = tmpl() + @depends(qux) + def foo(value): + return + + include(foo) + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "The dependency on `qux` is unused") + + def test_default_enable(self): + # --enable-* with default=True is not allowed. + with self.moz_configure( + """ + option('--enable-foo', default=False, help='foo') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--enable-foo', default=True, help='foo') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + "--disable-foo should be used instead of " "--enable-foo with default=True", + ) + + def test_default_disable(self): + # --disable-* with default=False is not allowed. + with self.moz_configure( + """ + option('--disable-foo', default=True, help='foo') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--disable-foo', default=False, help='foo') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + "--enable-foo should be used instead of " + "--disable-foo with default=False", + ) + + def test_default_with(self): + # --with-* with default=True is not allowed. + with self.moz_configure( + """ + option('--with-foo', default=False, help='foo') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--with-foo', default=True, help='foo') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + "--without-foo should be used instead of " "--with-foo with default=True", + ) + + def test_default_without(self): + # --without-* with default=False is not allowed. + with self.moz_configure( + """ + option('--without-foo', default=True, help='foo') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 2) as e: + with self.moz_configure( + """ + option('--without-foo', default=False, help='foo') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + "--with-foo should be used instead of " "--without-foo with default=False", + ) + + def test_default_func(self): + # Help text for an option with variable default should contain + # {enable|disable} rule. + with self.moz_configure( + """ + option(env='FOO', help='foo') + option('--enable-bar', default=depends('FOO')(lambda x: bool(x)), + help='{Enable|Disable} bar') + """ + ): + self.lint_test() + with self.assertRaisesFromLine(ConfigureError, 3) as e: + with self.moz_configure( + """ + option(env='FOO', help='foo') + option('--enable-bar', default=depends('FOO')(lambda x: bool(x)),\ + help='Enable bar') + """ + ): + self.lint_test() + self.assertEqual( + str(e.exception), + '`help` should contain "{Enable|Disable}" because of ' + "non-constant default", + ) + + def test_large_offset(self): + with self.assertRaisesFromLine(ConfigureError, 375): + with self.moz_configure( + """ + option(env='FOO', help='foo') + """ + + "\n" * 371 + + """ + option('--enable-bar', default=depends('FOO')(lambda x: bool(x)),\ + help='Enable bar') + """ + ): + self.lint_test() + + def test_undefined_global(self): + with self.assertRaisesFromLine(NameError, 6) as e: + with self.moz_configure( + """ + option(env='FOO', help='foo') + @depends('FOO') + def foo(value): + if value: + return unknown + return value + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "global name 'unknown' is not defined") + + # Ideally, this would raise on line 4, where `unknown` is used, but + # python disassembly doesn't give use the information. + with self.assertRaisesFromLine(NameError, 2) as e: + with self.moz_configure( + """ + @template + def tmpl(): + @depends(unknown) + def foo(value): + if value: + return True + return foo + tmpl() + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "global name 'unknown' is not defined") + + def test_unnecessary_imports(self): + with self.assertRaisesFromLine(NameError, 3) as e: + with self.moz_configure( + """ + option(env='FOO', help='foo') + @depends('FOO') + @imports(_from='__builtin__', _import='list') + def foo(value): + if value: + return list() + return value + """ + ): + self.lint_test() + + self.assertEqual(str(e.exception), "builtin 'list' doesn't need to be imported") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_moz_configure.py b/python/mozbuild/mozbuild/test/configure/test_moz_configure.py new file mode 100644 index 0000000000..22129a3970 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_moz_configure.py @@ -0,0 +1,185 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from mozunit import main + +from common import BaseConfigureTest, ConfigureTestSandbox +from mozbuild.util import ReadOnlyNamespace, exec_, memoized_property + + +def sandbox_class(platform): + class ConfigureTestSandboxOverridingPlatform(ConfigureTestSandbox): + @memoized_property + def _wrapped_sys(self): + sys = {} + exec_("from sys import *", sys) + sys["platform"] = platform + return ReadOnlyNamespace(**sys) + + return ConfigureTestSandboxOverridingPlatform + + +class TargetTest(BaseConfigureTest): + def get_target(self, args, env={}): + if "linux" in self.HOST: + platform = "linux2" + elif "mingw" in self.HOST or "windows" in self.HOST: + platform = "win32" + elif "openbsd6" in self.HOST: + platform = "openbsd6" + else: + raise Exception("Missing platform for HOST {}".format(self.HOST)) + sandbox = self.get_sandbox({}, {}, args, env, cls=sandbox_class(platform)) + return sandbox._value_for(sandbox["target"]).alias + + +class TestTargetLinux(TargetTest): + def test_target(self): + self.assertEqual(self.get_target([]), self.HOST) + self.assertEqual(self.get_target(["--target=i686"]), "i686-pc-linux-gnu") + self.assertEqual( + self.get_target(["--target=i686-unknown-linux-gnu"]), + "i686-unknown-linux-gnu", + ) + self.assertEqual( + self.get_target(["--target=i686-pc-windows-msvc"]), "i686-pc-windows-msvc" + ) + + +class TestTargetWindows(TargetTest): + # BaseConfigureTest uses this as the return value for config.guess + HOST = "i686-pc-windows-msvc" + + def test_target(self): + self.assertEqual(self.get_target([]), self.HOST) + self.assertEqual( + self.get_target(["--target=x86_64-pc-windows-msvc"]), + "x86_64-pc-windows-msvc", + ) + self.assertEqual(self.get_target(["--target=x86_64"]), "x86_64-pc-windows-msvc") + + # The tests above are actually not realistic, because most Windows + # machines will have a few environment variables that make us not + # use config.guess. + + # 32-bits process on x86_64 host. + env = { + "PROCESSOR_ARCHITECTURE": "x86", + "PROCESSOR_ARCHITEW6432": "AMD64", + } + self.assertEqual(self.get_target([], env), "x86_64-pc-windows-msvc") + self.assertEqual( + self.get_target(["--target=i686-pc-windows-msvc"]), "i686-pc-windows-msvc" + ) + self.assertEqual(self.get_target(["--target=i686"]), "i686-pc-windows-msvc") + + # 64-bits process on x86_64 host. + env = { + "PROCESSOR_ARCHITECTURE": "AMD64", + } + self.assertEqual(self.get_target([], env), "x86_64-pc-windows-msvc") + self.assertEqual( + self.get_target(["--target=i686-pc-windows-msvc"]), "i686-pc-windows-msvc" + ) + self.assertEqual(self.get_target(["--target=i686"]), "i686-pc-windows-msvc") + + # 32-bits process on x86 host. + env = { + "PROCESSOR_ARCHITECTURE": "x86", + } + self.assertEqual(self.get_target([], env), "i686-pc-windows-msvc") + self.assertEqual( + self.get_target(["--target=x86_64-pc-windows-msvc"]), + "x86_64-pc-windows-msvc", + ) + self.assertEqual(self.get_target(["--target=x86_64"]), "x86_64-pc-windows-msvc") + + # While host autodection will give us a -windows-msvc triplet, setting host + # is expecting to implicitly set the target. + self.assertEqual( + self.get_target(["--host=x86_64-pc-windows-gnu"]), "x86_64-pc-windows-gnu" + ) + self.assertEqual( + self.get_target(["--host=x86_64-pc-mingw32"]), "x86_64-pc-mingw32" + ) + + +class TestTargetAndroid(TargetTest): + HOST = "x86_64-pc-linux-gnu" + + def test_target(self): + self.assertEqual( + self.get_target(["--enable-project=mobile/android"]), + "arm-unknown-linux-androideabi", + ) + self.assertEqual( + self.get_target(["--enable-project=mobile/android", "--target=i686"]), + "i686-unknown-linux-android", + ) + self.assertEqual( + self.get_target(["--enable-project=mobile/android", "--target=x86_64"]), + "x86_64-unknown-linux-android", + ) + self.assertEqual( + self.get_target(["--enable-project=mobile/android", "--target=aarch64"]), + "aarch64-unknown-linux-android", + ) + self.assertEqual( + self.get_target(["--enable-project=mobile/android", "--target=arm"]), + "arm-unknown-linux-androideabi", + ) + + +class TestTargetOpenBSD(TargetTest): + # config.guess returns amd64 on OpenBSD, which we need to pass through to + # config.sub so that it canonicalizes to x86_64. + HOST = "amd64-unknown-openbsd6.4" + + def test_target(self): + self.assertEqual(self.get_target([]), "x86_64-unknown-openbsd6.4") + + def config_sub(self, stdin, args): + if args[0] == "amd64-unknown-openbsd6.4": + return 0, "x86_64-unknown-openbsd6.4", "" + return super(TestTargetOpenBSD, self).config_sub(stdin, args) + + +class TestMozConfigure(BaseConfigureTest): + def test_nsis_version(self): + this = self + + class FakeNSIS(object): + def __init__(self, version): + self.version = version + + def __call__(self, stdin, args): + this.assertEqual(args, ("-version",)) + return 0, self.version, "" + + def check_nsis_version(version): + sandbox = self.get_sandbox( + {"/usr/bin/makensis": FakeNSIS(version)}, + {}, + ["--target=x86_64-pc-windows-msvc", "--disable-bootstrap"], + {"PATH": "/usr/bin", "MAKENSISU": "/usr/bin/makensis"}, + ) + return sandbox._value_for(sandbox["nsis_version"]) + + with self.assertRaises(SystemExit): + check_nsis_version("v2.5") + + with self.assertRaises(SystemExit): + check_nsis_version("v3.0a2") + + self.assertEqual(check_nsis_version("v3.0b1"), "3.0b1") + self.assertEqual(check_nsis_version("v3.0b2"), "3.0b2") + self.assertEqual(check_nsis_version("v3.0rc1"), "3.0rc1") + self.assertEqual(check_nsis_version("v3.0"), "3.0") + self.assertEqual(check_nsis_version("v3.0-2"), "3.0") + self.assertEqual(check_nsis_version("v3.0.1"), "3.0") + self.assertEqual(check_nsis_version("v3.1"), "3.1") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_options.py b/python/mozbuild/mozbuild/test/configure/test_options.py new file mode 100644 index 0000000000..59ba616355 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_options.py @@ -0,0 +1,905 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import unittest + +from mozunit import main + +from mozbuild.configure.options import ( + CommandLineHelper, + ConflictingOptionError, + InvalidOptionError, + NegativeOptionValue, + Option, + OptionValue, + PositiveOptionValue, +) + + +class Option(Option): + def __init__(self, *args, **kwargs): + kwargs["help"] = "Dummy help" + super(Option, self).__init__(*args, **kwargs) + + +class TestOption(unittest.TestCase): + def test_option(self): + option = Option("--option") + self.assertEqual(option.prefix, "") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertFalse(option.default) + + option = Option("--enable-option") + self.assertEqual(option.prefix, "enable") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertFalse(option.default) + + option = Option("--disable-option") + self.assertEqual(option.prefix, "disable") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertTrue(option.default) + + option = Option("--with-option") + self.assertEqual(option.prefix, "with") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertFalse(option.default) + + option = Option("--without-option") + self.assertEqual(option.prefix, "without") + self.assertEqual(option.name, "option") + self.assertEqual(option.env, None) + self.assertTrue(option.default) + + option = Option("--without-option-foo", env="MOZ_OPTION") + self.assertEqual(option.env, "MOZ_OPTION") + + option = Option(env="MOZ_OPTION") + self.assertEqual(option.prefix, "") + self.assertEqual(option.name, None) + self.assertEqual(option.env, "MOZ_OPTION") + self.assertFalse(option.default) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=0, default=("a",)) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=1, default=()) + self.assertEqual( + str(e.exception), "default must be a bool, a string or a tuple of strings" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=1, default=True) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=1, default=("a", "b")) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=2, default=()) + self.assertEqual( + str(e.exception), "default must be a bool, a string or a tuple of strings" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=2, default=True) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=2, default=("a",)) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs="?", default=("a", "b")) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs="+", default=()) + self.assertEqual( + str(e.exception), "default must be a bool, a string or a tuple of strings" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs="+", default=True) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + # --disable options with a nargs value that requires at least one + # argument need to be given a default. + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs=1) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs="+") + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + # Test nargs inference from default value + option = Option("--with-foo", default=True) + self.assertEqual(option.nargs, 0) + + option = Option("--with-foo", default=False) + self.assertEqual(option.nargs, 0) + + option = Option("--with-foo", default="a") + self.assertEqual(option.nargs, "?") + + option = Option("--with-foo", default=("a",)) + self.assertEqual(option.nargs, "?") + + option = Option("--with-foo", default=("a", "b")) + self.assertEqual(option.nargs, "*") + + option = Option(env="FOO", default=True) + self.assertEqual(option.nargs, 0) + + option = Option(env="FOO", default=False) + self.assertEqual(option.nargs, 0) + + option = Option(env="FOO", default="a") + self.assertEqual(option.nargs, "?") + + option = Option(env="FOO", default=("a",)) + self.assertEqual(option.nargs, "?") + + option = Option(env="FOO", default=("a", "b")) + self.assertEqual(option.nargs, "*") + + def test_option_option(self): + for option in ( + "--option", + "--enable-option", + "--disable-option", + "--with-option", + "--without-option", + ): + self.assertEqual(Option(option).option, option) + self.assertEqual(Option(option, env="FOO").option, option) + + opt = Option(option, default=False) + self.assertEqual( + opt.option, + option.replace("-disable-", "-enable-").replace("-without-", "-with-"), + ) + + opt = Option(option, default=True) + self.assertEqual( + opt.option, + option.replace("-enable-", "-disable-").replace("-with-", "-without-"), + ) + + self.assertEqual(Option(env="FOO").option, "FOO") + + def test_option_choices(self): + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=3, choices=("a", "b")) + self.assertEqual(str(e.exception), "Not enough `choices` for `nargs`") + + with self.assertRaises(InvalidOptionError) as e: + Option("--without-option", nargs=1, choices=("a", "b")) + self.assertEqual( + str(e.exception), "A `default` must be given along with `choices`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--without-option", nargs="+", choices=("a", "b")) + self.assertEqual( + str(e.exception), "A `default` must be given along with `choices`" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--without-option", default="c", choices=("a", "b")) + self.assertEqual( + str(e.exception), "The `default` value must be one of 'a', 'b'" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option( + "--without-option", + default=( + "a", + "c", + ), + choices=("a", "b"), + ) + self.assertEqual( + str(e.exception), "The `default` value must be one of 'a', 'b'" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--without-option", default=("c",), choices=("a", "b")) + self.assertEqual( + str(e.exception), "The `default` value must be one of 'a', 'b'" + ) + + option = Option("--with-option", nargs="+", choices=("a", "b")) + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=c") + self.assertEqual(str(e.exception), "'c' is not one of 'a', 'b'") + + value = option.get_value("--with-option=b,a") + self.assertTrue(value) + self.assertEqual(PositiveOptionValue(("b", "a")), value) + + option = Option("--without-option", nargs="*", default="a", choices=("a", "b")) + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=c") + self.assertEqual(str(e.exception), "'c' is not one of 'a', 'b'") + + value = option.get_value("--with-option=b,a") + self.assertTrue(value) + self.assertEqual(PositiveOptionValue(("b", "a")), value) + + # Test nargs inference from choices + option = Option("--with-option", choices=("a", "b")) + self.assertEqual(option.nargs, 1) + + # Test "relative" values + option = Option( + "--with-option", nargs="*", default=("b", "c"), choices=("a", "b", "c", "d") + ) + + value = option.get_value("--with-option=+d") + self.assertEqual(PositiveOptionValue(("b", "c", "d")), value) + + value = option.get_value("--with-option=-b") + self.assertEqual(PositiveOptionValue(("c",)), value) + + value = option.get_value("--with-option=-b,+d") + self.assertEqual(PositiveOptionValue(("c", "d")), value) + + # Adding something that is in the default is fine + value = option.get_value("--with-option=+b") + self.assertEqual(PositiveOptionValue(("b", "c")), value) + + # Removing something that is not in the default is fine, as long as it + # is one of the choices + value = option.get_value("--with-option=-a") + self.assertEqual(PositiveOptionValue(("b", "c")), value) + + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=-e") + self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'") + + # Other "not a choice" errors. + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=+e") + self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'") + + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--with-option=e") + self.assertEqual(str(e.exception), "'e' is not one of 'a', 'b', 'c', 'd'") + + def test_option_value_compare(self): + # OptionValue are tuple and equivalence should compare as tuples. + val = PositiveOptionValue(("foo",)) + + self.assertEqual(val[0], "foo") + self.assertEqual(val, PositiveOptionValue(("foo",))) + self.assertNotEqual(val, PositiveOptionValue(("foo", "bar"))) + + # Can compare a tuple to an OptionValue. + self.assertEqual(val, ("foo",)) + self.assertNotEqual(val, ("foo", "bar")) + + # Different OptionValue types are never equal. + self.assertNotEqual(val, OptionValue(("foo",))) + + # For usability reasons, we raise TypeError when attempting to compare + # against a non-tuple. + with self.assertRaisesRegexp(TypeError, "cannot compare a"): + val == "foo" + + # But we allow empty option values to compare otherwise we can't + # easily compare value-less types like PositiveOptionValue and + # NegativeOptionValue. + empty_positive = PositiveOptionValue() + empty_negative = NegativeOptionValue() + self.assertEqual(empty_positive, ()) + self.assertEqual(empty_positive, PositiveOptionValue()) + self.assertEqual(empty_negative, ()) + self.assertEqual(empty_negative, NegativeOptionValue()) + self.assertNotEqual(empty_positive, "foo") + self.assertNotEqual(empty_positive, ("foo",)) + self.assertNotEqual(empty_negative, "foo") + self.assertNotEqual(empty_negative, ("foo",)) + + def test_option_value_format(self): + val = PositiveOptionValue() + self.assertEqual("--with-value", val.format("--with-value")) + self.assertEqual("--with-value", val.format("--without-value")) + self.assertEqual("--enable-value", val.format("--enable-value")) + self.assertEqual("--enable-value", val.format("--disable-value")) + self.assertEqual("--value", val.format("--value")) + self.assertEqual("VALUE=1", val.format("VALUE")) + + val = PositiveOptionValue(("a",)) + self.assertEqual("--with-value=a", val.format("--with-value")) + self.assertEqual("--with-value=a", val.format("--without-value")) + self.assertEqual("--enable-value=a", val.format("--enable-value")) + self.assertEqual("--enable-value=a", val.format("--disable-value")) + self.assertEqual("--value=a", val.format("--value")) + self.assertEqual("VALUE=a", val.format("VALUE")) + + val = PositiveOptionValue(("a", "b")) + self.assertEqual("--with-value=a,b", val.format("--with-value")) + self.assertEqual("--with-value=a,b", val.format("--without-value")) + self.assertEqual("--enable-value=a,b", val.format("--enable-value")) + self.assertEqual("--enable-value=a,b", val.format("--disable-value")) + self.assertEqual("--value=a,b", val.format("--value")) + self.assertEqual("VALUE=a,b", val.format("VALUE")) + + val = NegativeOptionValue() + self.assertEqual("--without-value", val.format("--with-value")) + self.assertEqual("--without-value", val.format("--without-value")) + self.assertEqual("--disable-value", val.format("--enable-value")) + self.assertEqual("--disable-value", val.format("--disable-value")) + self.assertEqual("", val.format("--value")) + self.assertEqual("VALUE=", val.format("VALUE")) + + def test_option_value(self, name="option", nargs=0, default=None): + disabled = name.startswith(("disable-", "without-")) + if disabled: + negOptionValue = PositiveOptionValue + posOptionValue = NegativeOptionValue + else: + posOptionValue = PositiveOptionValue + negOptionValue = NegativeOptionValue + defaultValue = PositiveOptionValue(default) if default else negOptionValue() + + option = Option("--%s" % name, nargs=nargs, default=default) + + if nargs in (0, "?", "*") or disabled: + value = option.get_value("--%s" % name, "option") + self.assertEqual(value, posOptionValue()) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s" % name) + if nargs == 1: + self.assertEqual(str(e.exception), "--%s takes 1 value" % name) + elif nargs == "+": + self.assertEqual(str(e.exception), "--%s takes 1 or more values" % name) + else: + self.assertEqual(str(e.exception), "--%s takes 2 values" % name) + + value = option.get_value("") + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + value = option.get_value(None) + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + with self.assertRaises(AssertionError): + value = option.get_value("MOZ_OPTION=", "environment") + + with self.assertRaises(AssertionError): + value = option.get_value("MOZ_OPTION=1", "environment") + + with self.assertRaises(AssertionError): + value = option.get_value("--foo") + + if nargs in (1, "?", "*", "+") and not disabled: + value = option.get_value("--%s=" % name, "option") + self.assertEqual(value, PositiveOptionValue(("",))) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s=" % name) + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + else: + self.assertEqual( + str(e.exception), "--%s takes %d values" % (name, nargs) + ) + + if nargs in (1, "?", "*", "+") and not disabled: + value = option.get_value("--%s=foo" % name, "option") + self.assertEqual(value, PositiveOptionValue(("foo",))) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s=foo" % name) + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + else: + self.assertEqual( + str(e.exception), "--%s takes %d values" % (name, nargs) + ) + + if nargs in (2, "*", "+") and not disabled: + value = option.get_value("--%s=foo,bar" % name, "option") + self.assertEqual(value, PositiveOptionValue(("foo", "bar"))) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s=foo,bar" % name, "option") + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + elif nargs == "?": + self.assertEqual(str(e.exception), "--%s takes 0 or 1 values" % name) + else: + self.assertEqual( + str(e.exception), + "--%s takes %d value%s" % (name, nargs, "s" if nargs != 1 else ""), + ) + + option = Option("--%s" % name, env="MOZ_OPTION", nargs=nargs, default=default) + if nargs in (0, "?", "*") or disabled: + value = option.get_value("--%s" % name, "option") + self.assertEqual(value, posOptionValue()) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s" % name) + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + elif nargs == "+": + self.assertEqual(str(e.exception), "--%s takes 1 or more values" % name) + else: + self.assertEqual( + str(e.exception), + "--%s takes %d value%s" % (name, nargs, "s" if nargs != 1 else ""), + ) + + value = option.get_value("") + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + value = option.get_value(None) + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + value = option.get_value("MOZ_OPTION=", "environment") + self.assertEqual(value, NegativeOptionValue()) + self.assertEqual(value.origin, "environment") + + if nargs in (0, "?", "*"): + value = option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(value, PositiveOptionValue()) + self.assertEqual(value.origin, "environment") + elif nargs in (1, "+"): + value = option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(value, PositiveOptionValue(("1",))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(str(e.exception), "MOZ_OPTION takes 2 values") + + if nargs in (1, "?", "*", "+") and not disabled: + value = option.get_value("--%s=" % name, "option") + self.assertEqual(value, PositiveOptionValue(("",))) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s=" % name, "option") + if disabled: + self.assertEqual(str(e.exception), "Cannot pass a value to --%s" % name) + else: + self.assertEqual( + str(e.exception), "--%s takes %d values" % (name, nargs) + ) + + with self.assertRaises(AssertionError): + value = option.get_value("--foo", "option") + + if nargs in (1, "?", "*", "+"): + value = option.get_value("MOZ_OPTION=foo", "environment") + self.assertEqual(value, PositiveOptionValue(("foo",))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("MOZ_OPTION=foo", "environment") + self.assertEqual(str(e.exception), "MOZ_OPTION takes %d values" % nargs) + + if nargs in (2, "*", "+"): + value = option.get_value("MOZ_OPTION=foo,bar", "environment") + self.assertEqual(value, PositiveOptionValue(("foo", "bar"))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("MOZ_OPTION=foo,bar", "environment") + if nargs == "?": + self.assertEqual(str(e.exception), "MOZ_OPTION takes 0 or 1 values") + else: + self.assertEqual( + str(e.exception), + "MOZ_OPTION takes %d value%s" % (nargs, "s" if nargs != 1 else ""), + ) + + if disabled: + return option + + env_option = Option(env="MOZ_OPTION", nargs=nargs, default=default) + with self.assertRaises(AssertionError): + env_option.get_value("--%s" % name) + + value = env_option.get_value("") + self.assertEqual(value, defaultValue) + self.assertEqual(value.origin, "default") + + value = env_option.get_value("MOZ_OPTION=", "environment") + self.assertEqual(value, negOptionValue()) + self.assertEqual(value.origin, "environment") + + if nargs in (0, "?", "*"): + value = env_option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(value, posOptionValue()) + self.assertTrue(value) + self.assertEqual(value.origin, "environment") + elif nargs in (1, "+"): + value = env_option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(value, PositiveOptionValue(("1",))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + env_option.get_value("MOZ_OPTION=1", "environment") + self.assertEqual(str(e.exception), "MOZ_OPTION takes 2 values") + + with self.assertRaises(AssertionError) as e: + env_option.get_value("--%s" % name) + + with self.assertRaises(AssertionError) as e: + env_option.get_value("--foo") + + if nargs in (1, "?", "*", "+"): + value = env_option.get_value("MOZ_OPTION=foo", "environment") + self.assertEqual(value, PositiveOptionValue(("foo",))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + env_option.get_value("MOZ_OPTION=foo", "environment") + self.assertEqual(str(e.exception), "MOZ_OPTION takes %d values" % nargs) + + if nargs in (2, "*", "+"): + value = env_option.get_value("MOZ_OPTION=foo,bar", "environment") + self.assertEqual(value, PositiveOptionValue(("foo", "bar"))) + self.assertEqual(value.origin, "environment") + else: + with self.assertRaises(InvalidOptionError) as e: + env_option.get_value("MOZ_OPTION=foo,bar", "environment") + if nargs == "?": + self.assertEqual(str(e.exception), "MOZ_OPTION takes 0 or 1 values") + else: + self.assertEqual( + str(e.exception), + "MOZ_OPTION takes %d value%s" % (nargs, "s" if nargs != 1 else ""), + ) + + return option + + def test_option_value_enable( + self, enable="enable", disable="disable", nargs=0, default=None + ): + option = self.test_option_value( + "%s-option" % enable, nargs=nargs, default=default + ) + + value = option.get_value("--%s-option" % disable, "option") + self.assertEqual(value, NegativeOptionValue()) + self.assertEqual(value.origin, "option") + + option = self.test_option_value( + "%s-option" % disable, nargs=nargs, default=default + ) + + if nargs in (0, "?", "*"): + value = option.get_value("--%s-option" % enable, "option") + self.assertEqual(value, PositiveOptionValue()) + self.assertEqual(value.origin, "option") + else: + with self.assertRaises(InvalidOptionError) as e: + option.get_value("--%s-option" % enable, "option") + if nargs == 1: + self.assertEqual(str(e.exception), "--%s-option takes 1 value" % enable) + elif nargs == "+": + self.assertEqual( + str(e.exception), "--%s-option takes 1 or more values" % enable + ) + else: + self.assertEqual( + str(e.exception), "--%s-option takes 2 values" % enable + ) + + def test_option_value_with(self): + self.test_option_value_enable("with", "without") + + def test_option_value_invalid_nargs(self): + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs="foo") + self.assertEqual( + str(e.exception), "nargs must be a positive integer, '?', '*' or '+'" + ) + + with self.assertRaises(InvalidOptionError) as e: + Option("--option", nargs=-2) + self.assertEqual( + str(e.exception), "nargs must be a positive integer, '?', '*' or '+'" + ) + + def test_option_value_nargs_1(self): + self.test_option_value(nargs=1) + self.test_option_value(nargs=1, default=("a",)) + self.test_option_value_enable(nargs=1, default=("a",)) + + # A default is required + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs=1) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + def test_option_value_nargs_2(self): + self.test_option_value(nargs=2) + self.test_option_value(nargs=2, default=("a", "b")) + self.test_option_value_enable(nargs=2, default=("a", "b")) + + # A default is required + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs=2) + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + def test_option_value_nargs_0_or_1(self): + self.test_option_value(nargs="?") + self.test_option_value(nargs="?", default=("a",)) + self.test_option_value_enable(nargs="?") + self.test_option_value_enable(nargs="?", default=("a",)) + + def test_option_value_nargs_0_or_more(self): + self.test_option_value(nargs="*") + self.test_option_value(nargs="*", default=("a",)) + self.test_option_value(nargs="*", default=("a", "b")) + self.test_option_value_enable(nargs="*") + self.test_option_value_enable(nargs="*", default=("a",)) + self.test_option_value_enable(nargs="*", default=("a", "b")) + + def test_option_value_nargs_1_or_more(self): + self.test_option_value(nargs="+") + self.test_option_value(nargs="+", default=("a",)) + self.test_option_value(nargs="+", default=("a", "b")) + self.test_option_value_enable(nargs="+", default=("a",)) + self.test_option_value_enable(nargs="+", default=("a", "b")) + + # A default is required + with self.assertRaises(InvalidOptionError) as e: + Option("--disable-option", nargs="+") + self.assertEqual( + str(e.exception), "The given `default` doesn't satisfy `nargs`" + ) + + +class TestCommandLineHelper(unittest.TestCase): + def test_basic(self): + helper = CommandLineHelper({}, ["cmd", "--foo", "--bar"]) + + self.assertEqual(["--foo", "--bar"], list(helper)) + + helper.add("--enable-qux") + + self.assertEqual(["--foo", "--bar", "--enable-qux"], list(helper)) + + value, option = helper.handle(Option("--bar")) + self.assertEqual(["--foo", "--enable-qux"], list(helper)) + self.assertEqual(PositiveOptionValue(), value) + self.assertEqual("--bar", option) + + value, option = helper.handle(Option("--baz")) + self.assertEqual(["--foo", "--enable-qux"], list(helper)) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual(None, option) + + with self.assertRaises(AssertionError): + CommandLineHelper({}, ["--foo", "--bar"]) + + def test_precedence(self): + foo = Option("--with-foo", nargs="*") + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--with-foo=a,b", option) + + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b", "--without-foo"]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--without-foo", option) + + helper = CommandLineHelper({}, ["cmd", "--without-foo", "--with-foo=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--with-foo=a,b", option) + + foo = Option("--with-foo", env="FOO", nargs="*") + helper = CommandLineHelper({"FOO": ""}, ["cmd", "--with-foo=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--with-foo=a,b", option) + + helper = CommandLineHelper({"FOO": "a,b"}, ["cmd", "--without-foo"]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--without-foo", option) + + helper = CommandLineHelper({"FOO": ""}, ["cmd", "--with-bar=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("environment", value.origin) + self.assertEqual("FOO=", option) + + helper = CommandLineHelper({"FOO": "a,b"}, ["cmd", "--without-bar"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("environment", value.origin) + self.assertEqual("FOO=a,b", option) + + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b", "FOO="]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("FOO=", option) + + helper = CommandLineHelper({}, ["cmd", "--without-foo", "FOO=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("FOO=a,b", option) + + helper = CommandLineHelper({}, ["cmd", "FOO=", "--with-foo=a,b"]) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b")), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--with-foo=a,b", option) + + helper = CommandLineHelper({}, ["cmd", "FOO=a,b", "--without-foo"]) + value, option = helper.handle(foo) + self.assertEqual(NegativeOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--without-foo", option) + + def test_extra_args(self): + foo = Option("--with-foo", env="FOO", nargs="*") + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b", "c")), value) + self.assertEqual("other-origin", value.origin) + self.assertEqual("FOO=a,b,c", option) + + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + helper.add("--with-foo=a,b,c", "other-origin") + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b", "c")), value) + self.assertEqual("other-origin", value.origin) + self.assertEqual("--with-foo=a,b,c", option) + + # Adding conflicting options is not allowed. + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.add("FOO=", "other-origin") + self.assertEqual("FOO=", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("FOO=a,b,c", cm.exception.old_arg) + self.assertEqual("other-origin", cm.exception.old_origin) + with self.assertRaises(ConflictingOptionError) as cm: + helper.add("FOO=a,b", "other-origin") + self.assertEqual("FOO=a,b", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("FOO=a,b,c", cm.exception.old_arg) + self.assertEqual("other-origin", cm.exception.old_origin) + # But adding the same is allowed. + helper.add("FOO=a,b,c", "other-origin") + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b", "c")), value) + self.assertEqual("other-origin", value.origin) + self.assertEqual("FOO=a,b,c", option) + + # The same rule as above applies when using the option form vs. the + # variable form. But we can't detect it when .add is called. + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + helper.add("--without-foo", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.handle(foo) + self.assertEqual("--without-foo", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("FOO=a,b,c", cm.exception.old_arg) + self.assertEqual("other-origin", cm.exception.old_origin) + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + helper.add("--with-foo=a,b", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.handle(foo) + self.assertEqual("--with-foo=a,b", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("FOO=a,b,c", cm.exception.old_arg) + self.assertEqual("other-origin", cm.exception.old_origin) + helper = CommandLineHelper({}, ["cmd"]) + helper.add("FOO=a,b,c", "other-origin") + helper.add("--with-foo=a,b,c", "other-origin") + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(("a", "b", "c")), value) + self.assertEqual("other-origin", value.origin) + self.assertEqual("--with-foo=a,b,c", option) + + # Conflicts are also not allowed against what is in the + # environment/on the command line. + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"]) + helper.add("FOO=a,b,c", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.handle(foo) + self.assertEqual("FOO=a,b,c", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("--with-foo=a,b", cm.exception.old_arg) + self.assertEqual("command-line", cm.exception.old_origin) + + helper = CommandLineHelper({}, ["cmd", "--with-foo=a,b"]) + helper.add("--without-foo", "other-origin") + with self.assertRaises(ConflictingOptionError) as cm: + helper.handle(foo) + self.assertEqual("--without-foo", cm.exception.arg) + self.assertEqual("other-origin", cm.exception.origin) + self.assertEqual("--with-foo=a,b", cm.exception.old_arg) + self.assertEqual("command-line", cm.exception.old_origin) + + def test_possible_origins(self): + with self.assertRaises(InvalidOptionError): + Option("--foo", possible_origins="command-line") + + helper = CommandLineHelper({"BAZ": "1"}, ["cmd", "--foo", "--bar"]) + foo = Option("--foo", possible_origins=("command-line",)) + value, option = helper.handle(foo) + self.assertEqual(PositiveOptionValue(), value) + self.assertEqual("command-line", value.origin) + self.assertEqual("--foo", option) + + bar = Option("--bar", possible_origins=("mozconfig",)) + with self.assertRaisesRegexp( + InvalidOptionError, + "--bar can not be set by command-line. Values are accepted from: mozconfig", + ): + helper.handle(bar) + + baz = Option(env="BAZ", possible_origins=("implied",)) + with self.assertRaisesRegexp( + InvalidOptionError, + "BAZ=1 can not be set by environment. Values are accepted from: implied", + ): + helper.handle(baz) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py b/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py new file mode 100644 index 0000000000..c6af3d99d4 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py @@ -0,0 +1,2056 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import logging +import os + +import six +from mozboot.util import MINIMUM_RUST_VERSION +from mozpack import path as mozpath +from mozunit import main +from six import StringIO +from test_toolchain_helpers import CompilerResult, FakeCompiler, PrependFlags + +from common import BaseConfigureTest +from mozbuild.configure.util import Version +from mozbuild.util import ReadOnlyNamespace, memoize + +DEFAULT_C99 = {"__STDC_VERSION__": "199901L"} + +DEFAULT_C11 = {"__STDC_VERSION__": "201112L"} + +DEFAULT_C17 = {"__STDC_VERSION__": "201710L"} + +DEFAULT_CXX_97 = {"__cplusplus": "199711L"} + +DEFAULT_CXX_11 = {"__cplusplus": "201103L"} + +DRAFT_CXX_14 = {"__cplusplus": "201300L"} + +DEFAULT_CXX_14 = {"__cplusplus": "201402L"} + +DRAFT_CXX17_201500 = {"__cplusplus": "201500L"} + +DRAFT_CXX17_201406 = {"__cplusplus": "201406L"} + +DEFAULT_CXX_17 = {"__cplusplus": "201703L"} + +SUPPORTS_GNU99 = {"-std=gnu99": DEFAULT_C99} + +SUPPORTS_GNUXX11 = {"-std=gnu++11": DEFAULT_CXX_11} + +SUPPORTS_GNUXX14 = {"-std=gnu++14": DEFAULT_CXX_14} + +SUPPORTS_CXX14 = {"-std=c++14": DEFAULT_CXX_14} + +SUPPORTS_GNUXX17 = {"-std=gnu++17": DEFAULT_CXX_17} + +SUPPORTS_CXX17 = {"-std=c++17": DEFAULT_CXX_17} + + +@memoize +def GCC_BASE(version): + version = Version(version) + return FakeCompiler( + { + "__GNUC__": version.major, + "__GNUC_MINOR__": version.minor, + "__GNUC_PATCHLEVEL__": version.patch, + "__STDC__": 1, + } + ) + + +@memoize +def GCC(version): + return GCC_BASE(version) + SUPPORTS_GNU99 + + +@memoize +def GXX(version): + return GCC_BASE(version) + DEFAULT_CXX_97 + SUPPORTS_GNUXX11 + + +SUPPORTS_DRAFT_CXX14_VERSION = {"-std=gnu++14": DRAFT_CXX_14} + +SUPPORTS_GNUXX1Z = {"-std=gnu++1z": DRAFT_CXX17_201406} + +SUPPORTS_DRAFT_CXX17_201500_VERSION = {"-std=gnu++17": DRAFT_CXX17_201500} + +GCC_4_9 = GCC("4.9.3") +GXX_4_9 = GXX("4.9.3") + SUPPORTS_DRAFT_CXX14_VERSION +GCC_5 = GCC("5.2.1") + DEFAULT_C11 +GXX_5 = GXX("5.2.1") + SUPPORTS_GNUXX14 +GCC_6 = GCC("6.4.0") + DEFAULT_C11 +GXX_6 = ( + GXX("6.4.0") + + DEFAULT_CXX_14 + + SUPPORTS_GNUXX17 + + SUPPORTS_DRAFT_CXX17_201500_VERSION +) +GCC_7 = GCC("7.3.0") + DEFAULT_C11 +GXX_7 = GXX("7.3.0") + DEFAULT_CXX_14 + SUPPORTS_GNUXX17 + SUPPORTS_CXX17 +GCC_8 = GCC("8.3.0") + DEFAULT_C11 +GXX_8 = GXX("8.3.0") + DEFAULT_CXX_14 + SUPPORTS_GNUXX17 + SUPPORTS_CXX17 +GCC_10 = GCC("10.2.1") + DEFAULT_C17 +GXX_10 = GXX("10.2.1") + DEFAULT_CXX_14 + SUPPORTS_GNUXX17 + SUPPORTS_CXX17 + +DEFAULT_GCC = GCC_8 +DEFAULT_GXX = GXX_8 + +GCC_PLATFORM_LITTLE_ENDIAN = { + "__ORDER_LITTLE_ENDIAN__": 1234, + "__ORDER_BIG_ENDIAN__": 4321, + "__BYTE_ORDER__": 1234, +} + +GCC_PLATFORM_BIG_ENDIAN = { + "__ORDER_LITTLE_ENDIAN__": 1234, + "__ORDER_BIG_ENDIAN__": 4321, + "__BYTE_ORDER__": 4321, +} + +GCC_PLATFORM_X86 = FakeCompiler(GCC_PLATFORM_LITTLE_ENDIAN) + { + None: {"__i386__": 1}, + "-m64": {"__i386__": False, "__x86_64__": 1}, +} + +GCC_PLATFORM_X86_64 = FakeCompiler(GCC_PLATFORM_LITTLE_ENDIAN) + { + None: {"__x86_64__": 1}, + "-m32": {"__x86_64__": False, "__i386__": 1}, +} + +GCC_PLATFORM_ARM = FakeCompiler(GCC_PLATFORM_LITTLE_ENDIAN) + {"__arm__": 1} + +GCC_PLATFORM_LINUX = {"__linux__": 1} + +GCC_PLATFORM_DARWIN = {"__APPLE__": 1} + +GCC_PLATFORM_WIN = {"_WIN32": 1, "WINNT": 1} + +GCC_PLATFORM_OPENBSD = {"__OpenBSD__": 1} + +GCC_PLATFORM_X86_LINUX = FakeCompiler(GCC_PLATFORM_X86, GCC_PLATFORM_LINUX) +GCC_PLATFORM_X86_64_LINUX = FakeCompiler(GCC_PLATFORM_X86_64, GCC_PLATFORM_LINUX) +GCC_PLATFORM_ARM_LINUX = FakeCompiler(GCC_PLATFORM_ARM, GCC_PLATFORM_LINUX) +GCC_PLATFORM_X86_OSX = FakeCompiler(GCC_PLATFORM_X86, GCC_PLATFORM_DARWIN) +GCC_PLATFORM_X86_64_OSX = FakeCompiler(GCC_PLATFORM_X86_64, GCC_PLATFORM_DARWIN) +GCC_PLATFORM_X86_WIN = FakeCompiler(GCC_PLATFORM_X86, GCC_PLATFORM_WIN) +GCC_PLATFORM_X86_64_WIN = FakeCompiler(GCC_PLATFORM_X86_64, GCC_PLATFORM_WIN) + + +@memoize +def CLANG_BASE(version): + version = Version(version) + return FakeCompiler( + { + "__clang__": 1, + "__clang_major__": version.major, + "__clang_minor__": version.minor, + "__clang_patchlevel__": version.patch, + } + ) + + +@memoize +def CLANG(version): + return GCC_BASE("4.2.1") + CLANG_BASE(version) + SUPPORTS_GNU99 + + +@memoize +def CLANGXX(version): + return ( + GCC_BASE("4.2.1") + + CLANG_BASE(version) + + DEFAULT_CXX_97 + + SUPPORTS_GNUXX11 + + SUPPORTS_GNUXX14 + ) + + +CLANG_3_3 = CLANG("3.3.0") + DEFAULT_C99 +CLANGXX_3_3 = CLANGXX("3.3.0") +CLANG_4_0 = CLANG("4.0.2") + DEFAULT_C11 +CLANGXX_4_0 = CLANGXX("4.0.2") + SUPPORTS_GNUXX1Z +CLANG_7_0 = CLANG("7.0.0") + DEFAULT_C11 +CLANGXX_7_0 = CLANGXX("7.0.0") + DEFAULT_CXX_14 + SUPPORTS_GNUXX17 +XCODE_CLANG_3_3 = ( + CLANG("5.0") + + DEFAULT_C99 + + { + # Real Xcode clang has a full version here, but we don't care about it. + "__apple_build_version__": "1" + } +) +XCODE_CLANGXX_3_3 = CLANGXX("5.0") + {"__apple_build_version__": "1"} +XCODE_CLANG_4_0 = CLANG("9.0.0") + DEFAULT_C11 + {"__apple_build_version__": "1"} +XCODE_CLANGXX_4_0 = ( + CLANGXX("9.0.0") + SUPPORTS_GNUXX1Z + {"__apple_build_version__": "1"} +) +XCODE_CLANG_7_0 = CLANG("10.0.1") + DEFAULT_C11 + {"__apple_build_version__": "1"} +XCODE_CLANGXX_7_0 = ( + CLANGXX("10.0.1") + SUPPORTS_GNUXX17 + {"__apple_build_version__": "1"} +) +DEFAULT_CLANG = CLANG_7_0 +DEFAULT_CLANGXX = CLANGXX_7_0 + + +def CLANG_PLATFORM(gcc_platform): + base = { + "--target=x86_64-linux-gnu": GCC_PLATFORM_X86_64_LINUX[None], + "--target=x86_64-apple-darwin11.2.0": GCC_PLATFORM_X86_64_OSX[None], + "--target=i686-linux-gnu": GCC_PLATFORM_X86_LINUX[None], + "--target=i686-apple-darwin11.2.0": GCC_PLATFORM_X86_OSX[None], + "--target=arm-linux-gnu": GCC_PLATFORM_ARM_LINUX[None], + } + undo_gcc_platform = { + k: {symbol: False for symbol in gcc_platform[None]} for k in base + } + return FakeCompiler(gcc_platform, undo_gcc_platform, base) + + +CLANG_PLATFORM_X86_LINUX = CLANG_PLATFORM(GCC_PLATFORM_X86_LINUX) +CLANG_PLATFORM_X86_64_LINUX = CLANG_PLATFORM(GCC_PLATFORM_X86_64_LINUX) +CLANG_PLATFORM_X86_OSX = CLANG_PLATFORM(GCC_PLATFORM_X86_OSX) +CLANG_PLATFORM_X86_64_OSX = CLANG_PLATFORM(GCC_PLATFORM_X86_64_OSX) +CLANG_PLATFORM_X86_WIN = CLANG_PLATFORM(GCC_PLATFORM_X86_WIN) +CLANG_PLATFORM_X86_64_WIN = CLANG_PLATFORM(GCC_PLATFORM_X86_64_WIN) + + +@memoize +def VS(version): + version = Version(version) + return FakeCompiler( + { + None: { + "_MSC_VER": "%02d%02d" % (version.major, version.minor), + "_MSC_FULL_VER": "%02d%02d%05d" + % (version.major, version.minor, version.patch), + "_MT": "1", + }, + "*.cpp": DEFAULT_CXX_97, + } + ) + + +VS_2017u8 = VS("19.15.26726") + +VS_PLATFORM_X86 = {"_M_IX86": 600, "_WIN32": 1} + +VS_PLATFORM_X86_64 = {"_M_X64": 100, "_WIN32": 1, "_WIN64": 1} + +# Despite the 32 in the name, this macro is defined for 32- and 64-bit. +MINGW32 = {"__MINGW32__": True} + +# Note: In reality, the -std=gnu* options are only supported when preceded by +# -Xclang. +CLANG_CL_3_9 = ( + CLANG_BASE("3.9.0") + + VS("18.00.00000") + + DEFAULT_C11 + + SUPPORTS_GNU99 + + SUPPORTS_GNUXX11 + + SUPPORTS_CXX14 +) + {"*.cpp": {"__STDC_VERSION__": False, "__cplusplus": "201103L"}} +CLANG_CL_9_0 = ( + CLANG_BASE("9.0.0") + + VS("18.00.00000") + + DEFAULT_C11 + + SUPPORTS_GNU99 + + SUPPORTS_GNUXX11 + + SUPPORTS_CXX14 + + SUPPORTS_CXX17 +) + {"*.cpp": {"__STDC_VERSION__": False, "__cplusplus": "201103L"}} + +CLANG_CL_PLATFORM_X86 = FakeCompiler( + VS_PLATFORM_X86, GCC_PLATFORM_X86[None], GCC_PLATFORM_LITTLE_ENDIAN +) +CLANG_CL_PLATFORM_X86_64 = FakeCompiler( + VS_PLATFORM_X86_64, GCC_PLATFORM_X86_64[None], GCC_PLATFORM_LITTLE_ENDIAN +) + +LIBRARY_NAME_INFOS = { + "linux-gnu": { + "DLL_PREFIX": "lib", + "DLL_SUFFIX": ".so", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "", + "OBJ_SUFFIX": "o", + }, + "darwin11.2.0": { + "DLL_PREFIX": "lib", + "DLL_SUFFIX": ".dylib", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "", + "OBJ_SUFFIX": "o", + }, + "mingw32": { + "DLL_PREFIX": "", + "DLL_SUFFIX": ".dll", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "a", + "OBJ_SUFFIX": "o", + }, + "windows-msvc": { + "DLL_PREFIX": "", + "DLL_SUFFIX": ".dll", + "LIB_PREFIX": "", + "LIB_SUFFIX": "lib", + "IMPORT_LIB_SUFFIX": "lib", + "OBJ_SUFFIX": "obj", + }, + "windows-gnu": { + "DLL_PREFIX": "", + "DLL_SUFFIX": ".dll", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "a", + "OBJ_SUFFIX": "o", + }, + "openbsd6.1": { + "DLL_PREFIX": "lib", + "DLL_SUFFIX": ".so.1.0", + "LIB_PREFIX": "lib", + "LIB_SUFFIX": "a", + "IMPORT_LIB_SUFFIX": "", + "OBJ_SUFFIX": "o", + }, +} + + +class BaseToolchainTest(BaseConfigureTest): + def setUp(self): + super(BaseToolchainTest, self).setUp() + self.out = StringIO() + self.logger = logging.getLogger("BaseToolchainTest") + self.logger.setLevel(logging.ERROR) + self.handler = logging.StreamHandler(self.out) + self.logger.addHandler(self.handler) + + def tearDown(self): + self.logger.removeHandler(self.handler) + del self.handler + del self.out + super(BaseToolchainTest, self).tearDown() + + def do_toolchain_test(self, paths, results, args=[], environ={}): + """Helper to test the toolchain checks from toolchain.configure. + + - `paths` is a dict associating compiler paths to FakeCompiler + definitions from above. + - `results` is a dict associating result variable names from + toolchain.configure (c_compiler, cxx_compiler, host_c_compiler, + host_cxx_compiler) with a result. + The result can either be an error string, or a CompilerResult + corresponding to the object returned by toolchain.configure checks. + When the results for host_c_compiler are identical to c_compiler, + they can be omitted. Likewise for host_cxx_compiler vs. + cxx_compiler. + """ + environ = dict(environ) + if "PATH" not in environ: + environ["PATH"] = os.pathsep.join( + mozpath.abspath(p) for p in ("/bin", "/usr/bin") + ) + + args = args + ["--enable-release", "--disable-bootstrap"] + + sandbox = self.get_sandbox(paths, {}, args, environ, logger=self.logger) + + for var in ( + "c_compiler", + "cxx_compiler", + "host_c_compiler", + "host_cxx_compiler", + ): + if var in results: + result = results[var] + elif var.startswith("host_"): + result = results.get(var[5:], {}) + else: + result = {} + try: + self.out.truncate(0) + self.out.seek(0) + compiler = sandbox._value_for(sandbox[var]) + # Add var on both ends to make it clear which of the + # variables is failing the test when that happens. + self.assertEqual((var, compiler), (var, result)) + except SystemExit: + self.assertEqual((var, result), (var, self.out.getvalue().strip())) + return + + # Normalize the target os to match what we have as keys in + # LIBRARY_NAME_INFOS. + target_os = getattr(self, "TARGET", self.HOST).split("-", 2)[2] + if target_os == "mingw32": + compiler_type = sandbox._value_for(sandbox["c_compiler"]).type + if compiler_type == "clang-cl": + target_os = "windows-msvc" + elif target_os == "linux-gnuabi64": + target_os = "linux-gnu" + + self.do_library_name_info_test(target_os, sandbox) + + # Try again on artifact builds. In that case, we always get library + # name info for msvc on Windows + if target_os == "mingw32": + target_os = "windows-msvc" + + sandbox = self.get_sandbox( + paths, {}, args + ["--enable-artifact-builds"], environ, logger=self.logger + ) + + self.do_library_name_info_test(target_os, sandbox) + + def do_library_name_info_test(self, target_os, sandbox): + library_name_info = LIBRARY_NAME_INFOS[target_os] + for k in ( + "DLL_PREFIX", + "DLL_SUFFIX", + "LIB_PREFIX", + "LIB_SUFFIX", + "IMPORT_LIB_SUFFIX", + "OBJ_SUFFIX", + ): + self.assertEqual( + "%s=%s" % (k, sandbox.get_config(k)), + "%s=%s" % (k, library_name_info[k]), + ) + + +def old_gcc_message(old_ver): + return "Only GCC 8.1 or newer is supported (found version {}).".format(old_ver) + + +class LinuxToolchainTest(BaseToolchainTest): + PATHS = { + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-4.9": GCC_4_9 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-4.9": GXX_4_9 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-5": GCC_5 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-5": GXX_5 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-6": GCC_6 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-6": GXX_6 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-7": GCC_7 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-7": GXX_7 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-8": GCC_8 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-8": GXX_8 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/gcc-10": GCC_10 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/g++-10": GXX_10 + GCC_PLATFORM_X86_64_LINUX, + "/usr/bin/clang": DEFAULT_CLANG + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++": DEFAULT_CLANGXX + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang-7.0": CLANG_7_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++-7.0": CLANGXX_7_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang-4.0": CLANG_4_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++-4.0": CLANGXX_4_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang-3.3": CLANG_3_3 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++-3.3": CLANGXX_3_3 + CLANG_PLATFORM_X86_64_LINUX, + } + + GCC_4_7_RESULT = old_gcc_message("4.7.3") + GXX_4_7_RESULT = GCC_4_7_RESULT + GCC_4_9_RESULT = old_gcc_message("4.9.3") + GXX_4_9_RESULT = GCC_4_9_RESULT + GCC_5_RESULT = old_gcc_message("5.2.1") + GXX_5_RESULT = GCC_5_RESULT + GCC_6_RESULT = old_gcc_message("6.4.0") + GXX_6_RESULT = GCC_6_RESULT + GCC_7_RESULT = old_gcc_message("7.3.0") + GXX_7_RESULT = GCC_7_RESULT + GCC_8_RESULT = CompilerResult( + flags=["-std=gnu99"], + version="8.3.0", + type="gcc", + compiler="/usr/bin/gcc-8", + language="C", + ) + GXX_8_RESULT = CompilerResult( + flags=["-std=gnu++17"], + version="8.3.0", + type="gcc", + compiler="/usr/bin/g++-8", + language="C++", + ) + DEFAULT_GCC_RESULT = GCC_8_RESULT + {"compiler": "/usr/bin/gcc"} + DEFAULT_GXX_RESULT = GXX_8_RESULT + {"compiler": "/usr/bin/g++"} + + CLANG_3_3_RESULT = ( + "Only clang/llvm 7.0 or newer is supported (found version 3.3.0)." + ) + CLANGXX_3_3_RESULT = ( + "Only clang/llvm 7.0 or newer is supported (found version 3.3.0)." + ) + CLANG_4_0_RESULT = ( + "Only clang/llvm 7.0 or newer is supported (found version 4.0.2)." + ) + CLANGXX_4_0_RESULT = ( + "Only clang/llvm 7.0 or newer is supported (found version 4.0.2)." + ) + CLANG_7_0_RESULT = CompilerResult( + flags=["-std=gnu99"], + version="7.0.0", + type="clang", + compiler="/usr/bin/clang-7.0", + language="C", + ) + CLANGXX_7_0_RESULT = CompilerResult( + flags=["-std=gnu++17"], + version="7.0.0", + type="clang", + compiler="/usr/bin/clang++-7.0", + language="C++", + ) + DEFAULT_CLANG_RESULT = CLANG_7_0_RESULT + {"compiler": "/usr/bin/clang"} + DEFAULT_CLANGXX_RESULT = CLANGXX_7_0_RESULT + {"compiler": "/usr/bin/clang++"} + + def test_default(self): + # We'll try clang and gcc, and find clang first. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + def test_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "gcc", "CXX": "g++"}, + ) + + def test_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.GCC_4_9_RESULT}, + environ={"CC": "gcc-4.9", "CXX": "g++-4.9"}, + ) + + # Maybe this should be reporting the mismatched version instead. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": self.GXX_4_9_RESULT, + }, + environ={"CC": "gcc", "CXX": "g++-4.9"}, + ) + + def test_overridden_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.GCC_7_RESULT, "cxx_compiler": self.GXX_7_RESULT}, + environ={"CC": "gcc-7", "CXX": "g++-7"}, + ) + + def test_guess_cxx(self): + # When CXX is not set, we guess it from CC. + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.GCC_7_RESULT, "cxx_compiler": self.GXX_7_RESULT}, + environ={"CC": "gcc-7"}, + ) + + def test_mismatched_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": ( + "The target C compiler is version 8.3.0, while the target " + "C++ compiler is version 10.2.1. Need to use the same compiler " + "version." + ), + }, + environ={"CC": "gcc", "CXX": "g++-10"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": self.DEFAULT_GXX_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": ( + "The host C compiler is version 8.3.0, while the host " + "C++ compiler is version 10.2.1. Need to use the same compiler " + "version." + ), + }, + environ={"CC": "gcc", "HOST_CXX": "g++-10"}, + ) + + def test_mismatched_compiler(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": ( + "The target C compiler is clang, while the target C++ compiler " + "is gcc. Need to use the same compiler suite." + ), + }, + environ={"CXX": "g++"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": ( + "The host C compiler is clang, while the host C++ compiler " + "is gcc. Need to use the same compiler suite." + ), + }, + environ={"HOST_CXX": "g++"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": "`%s` is not a C compiler." + % mozpath.abspath("/usr/bin/g++") + }, + environ={"CC": "g++"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": "`%s` is not a C++ compiler." + % mozpath.abspath("/usr/bin/clang"), + }, + environ={"CXX": "clang"}, + ) + + def test_clang(self): + # We'll try gcc and clang, but since there is no gcc (gcc-x.y doesn't + # count), find clang. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("gcc", "g++") + } + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + def test_guess_cxx_clang(self): + # When CXX is not set, we guess it from CC. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_7_0_RESULT, + "cxx_compiler": self.CLANGXX_7_0_RESULT, + }, + environ={"CC": "clang-7.0"}, + ) + + def test_unsupported_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_3_3_RESULT, + "cxx_compiler": self.CLANGXX_3_3_RESULT, + }, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_4_0_RESULT, + "cxx_compiler": self.CLANGXX_4_0_RESULT, + }, + environ={"CC": "clang-4.0", "CXX": "clang++-4.0"}, + ) + + def test_no_supported_compiler(self): + # Even if there are gcc-x.y or clang-x.y compilers available, we + # don't try them. This could be considered something to improve. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("gcc", "g++", "clang", "clang++") + } + self.do_toolchain_test( + paths, {"c_compiler": "Cannot find the target C compiler"} + ) + + def test_absolute_path(self): + paths = dict(self.PATHS) + paths.update( + { + "/opt/clang/bin/clang": paths["/usr/bin/clang"], + "/opt/clang/bin/clang++": paths["/usr/bin/clang++"], + } + ) + result = { + "c_compiler": self.DEFAULT_CLANG_RESULT + + {"compiler": "/opt/clang/bin/clang"}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + + {"compiler": "/opt/clang/bin/clang++"}, + } + self.do_toolchain_test( + paths, + result, + environ={"CC": "/opt/clang/bin/clang", "CXX": "/opt/clang/bin/clang++"}, + ) + # With CXX guess too. + self.do_toolchain_test(paths, result, environ={"CC": "/opt/clang/bin/clang"}) + + def test_atypical_name(self): + paths = dict(self.PATHS) + paths.update( + { + "/usr/bin/afl-clang-fast": paths["/usr/bin/clang"], + "/usr/bin/afl-clang-fast++": paths["/usr/bin/clang++"], + } + ) + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + + {"compiler": "/usr/bin/afl-clang-fast"}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + + {"compiler": "/usr/bin/afl-clang-fast++"}, + }, + environ={"CC": "afl-clang-fast", "CXX": "afl-clang-fast++"}, + ) + + def test_mixed_compilers(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "clang", "HOST_CC": "gcc"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "clang", "CXX": "clang++", "HOST_CC": "gcc"}, + ) + + +class LinuxSimpleCrossToolchainTest(BaseToolchainTest): + TARGET = "i686-pc-linux-gnu" + PATHS = LinuxToolchainTest.PATHS + DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + + def test_cross_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT + {"flags": ["-m32"]}, + "cxx_compiler": self.DEFAULT_GXX_RESULT + {"flags": ["-m32"]}, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "gcc"}, + ) + + def test_cross_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + {"flags": ["-m32"]}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + {"flags": ["-m32"]}, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + +class LinuxX86_64CrossToolchainTest(BaseToolchainTest): + HOST = "i686-pc-linux-gnu" + TARGET = "x86_64-pc-linux-gnu" + PATHS = { + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_LINUX, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_LINUX, + "/usr/bin/clang": DEFAULT_CLANG + CLANG_PLATFORM_X86_LINUX, + "/usr/bin/clang++": DEFAULT_CLANGXX + CLANG_PLATFORM_X86_LINUX, + } + DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + + def test_cross_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT + {"flags": ["-m64"]}, + "cxx_compiler": self.DEFAULT_GXX_RESULT + {"flags": ["-m64"]}, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "gcc"}, + ) + + def test_cross_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + {"flags": ["-m64"]}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + {"flags": ["-m64"]}, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + +def xcrun(stdin, args): + if args == ("--show-sdk-path",): + return ( + 0, + mozpath.join(os.path.abspath(os.path.dirname(__file__)), "macos_fake_sdk"), + "", + ) + raise NotImplementedError() + + +class OSXToolchainTest(BaseToolchainTest): + HOST = "x86_64-apple-darwin11.2.0" + PATHS = { + "/usr/bin/gcc-5": GCC_5 + GCC_PLATFORM_X86_64_OSX, + "/usr/bin/g++-5": GXX_5 + GCC_PLATFORM_X86_64_OSX, + "/usr/bin/gcc-8": GCC_8 + GCC_PLATFORM_X86_64_OSX, + "/usr/bin/g++-8": GXX_8 + GCC_PLATFORM_X86_64_OSX, + "/usr/bin/clang": XCODE_CLANG_7_0 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang++": XCODE_CLANGXX_7_0 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang-4.0": XCODE_CLANG_4_0 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang++-4.0": XCODE_CLANGXX_4_0 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang-3.3": XCODE_CLANG_3_3 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/clang++-3.3": XCODE_CLANGXX_3_3 + CLANG_PLATFORM_X86_64_OSX, + "/usr/bin/xcrun": xcrun, + } + CLANG_3_3_RESULT = ( + "Only clang/llvm 7.0 or newer is supported (found version 4.0.0.or.less)." + ) + CLANGXX_3_3_RESULT = ( + "Only clang/llvm 7.0 or newer is supported (found version 4.0.0.or.less)." + ) + CLANG_4_0_RESULT = ( + "Only clang/llvm 7.0 or newer is supported (found version 4.0.0.or.less)." + ) + CLANGXX_4_0_RESULT = ( + "Only clang/llvm 7.0 or newer is supported (found version 4.0.0.or.less)." + ) + DEFAULT_CLANG_RESULT = CompilerResult( + flags=["-std=gnu99"], + version="7.0.0", + type="clang", + compiler="/usr/bin/clang", + language="C", + ) + DEFAULT_CLANGXX_RESULT = CompilerResult( + flags=["-stdlib=libc++", "-std=gnu++17"], + version="7.0.0", + type="clang", + compiler="/usr/bin/clang++", + language="C++", + ) + GCC_5_RESULT = LinuxToolchainTest.GCC_5_RESULT + GXX_5_RESULT = LinuxToolchainTest.GXX_5_RESULT + GCC_8_RESULT = LinuxToolchainTest.GCC_8_RESULT + GXX_8_RESULT = LinuxToolchainTest.GXX_8_RESULT + SYSROOT_FLAGS = { + "flags": PrependFlags( + [ + "-isysroot", + xcrun("", ("--show-sdk-path",))[1], + "-mmacosx-version-min=10.12", + ] + ) + } + + def test_clang(self): + # We only try clang because gcc is known not to work. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + self.SYSROOT_FLAGS, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + self.SYSROOT_FLAGS, + }, + ) + + def test_not_gcc(self): + # We won't pick GCC if it's the only thing available. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("clang", "clang++") + } + self.do_toolchain_test( + paths, {"c_compiler": "Cannot find the target C compiler"} + ) + + def test_unsupported_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_3_3_RESULT, + "cxx_compiler": self.CLANGXX_3_3_RESULT, + }, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + # When targeting mac, we require at least version 5. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_4_0_RESULT, + "cxx_compiler": self.CLANGXX_4_0_RESULT, + }, + environ={"CC": "clang-4.0", "CXX": "clang++-4.0"}, + ) + + def test_forced_gcc(self): + # GCC can still be forced if the user really wants it. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.GCC_8_RESULT + self.SYSROOT_FLAGS, + "cxx_compiler": self.GXX_8_RESULT + self.SYSROOT_FLAGS, + }, + environ={"CC": "gcc-8", "CXX": "g++-8"}, + ) + + def test_forced_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.GCC_5_RESULT}, + environ={"CC": "gcc-5", "CXX": "g++-5"}, + ) + + +class MingwToolchainTest(BaseToolchainTest): + HOST = "i686-pc-mingw32" + + # For the purpose of this test, it doesn't matter that the paths are not + # real Windows paths. + PATHS = { + "/usr/bin/cl": VS_2017u8 + VS_PLATFORM_X86, + "/usr/bin/clang-cl-3.9": CLANG_CL_3_9 + CLANG_CL_PLATFORM_X86, + "/usr/bin/clang-cl": CLANG_CL_9_0 + CLANG_CL_PLATFORM_X86, + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/gcc-4.9": GCC_4_9 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++-4.9": GXX_4_9 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/gcc-5": GCC_5 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++-5": GXX_5 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/gcc-6": GCC_6 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++-6": GXX_6 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/gcc-7": GCC_7 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/g++-7": GXX_7 + GCC_PLATFORM_X86_WIN + MINGW32, + "/usr/bin/clang": DEFAULT_CLANG + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang++": DEFAULT_CLANGXX + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang-7.0": CLANG_7_0 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang++-7.0": CLANGXX_7_0 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang-4.0": CLANG_4_0 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang++-4.0": CLANGXX_4_0 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang-3.3": CLANG_3_3 + CLANG_PLATFORM_X86_WIN, + "/usr/bin/clang++-3.3": CLANGXX_3_3 + CLANG_PLATFORM_X86_WIN, + } + + CLANG_CL_3_9_RESULT = ( + "Only clang-cl 9.0 or newer is supported (found version 3.9.0)" + ) + CLANG_CL_9_0_RESULT = CompilerResult( + version="9.0.0", + flags=["-Xclang", "-std=gnu99"], + type="clang-cl", + compiler="/usr/bin/clang-cl", + language="C", + ) + CLANGXX_CL_3_9_RESULT = ( + "Only clang-cl 9.0 or newer is supported (found version 3.9.0)" + ) + CLANGXX_CL_9_0_RESULT = CompilerResult( + version="9.0.0", + flags=["-Xclang", "-std=c++17"], + type="clang-cl", + compiler="/usr/bin/clang-cl", + language="C++", + ) + CLANG_3_3_RESULT = LinuxToolchainTest.CLANG_3_3_RESULT + CLANGXX_3_3_RESULT = LinuxToolchainTest.CLANGXX_3_3_RESULT + CLANG_4_0_RESULT = LinuxToolchainTest.CLANG_4_0_RESULT + CLANGXX_4_0_RESULT = LinuxToolchainTest.CLANGXX_4_0_RESULT + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + + def test_unsupported_msvc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "/usr/bin/cl"}, + ) + + def test_unsupported_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.CLANG_CL_3_9_RESULT}, + environ={"CC": "/usr/bin/clang-cl-3.9"}, + ) + + def test_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_CL_9_0_RESULT, + "cxx_compiler": self.CLANGXX_CL_9_0_RESULT, + }, + ) + + def test_gcc(self): + # GCC is unsupported, if you try it should find clang. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) != "clang-cl" + } + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + # This test is not perfect, as the GCC version needs to be updated when we + # bump the minimum GCC version, but the idea is that even supported GCC + # on other platforms should not be supported on Windows. + def test_overridden_supported_elsewhere_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "gcc-7", "CXX": "g++-7"}, + ) + + def test_overridden_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "gcc-5", "CXX": "g++-5"}, + ) + + def test_clang(self): + # We'll pick clang if nothing else is found. + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("clang-cl", "gcc") + } + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_CLANG_RESULT, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + def test_overridden_unsupported_clang(self): + # clang 3.3 C compiler is perfectly fine, but we need more for C++. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.CLANG_3_3_RESULT, + "cxx_compiler": self.CLANGXX_3_3_RESULT, + }, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + + +class Mingw64ToolchainTest(MingwToolchainTest): + HOST = "x86_64-pc-mingw32" + + # For the purpose of this test, it doesn't matter that the paths are not + # real Windows paths. + PATHS = { + "/usr/bin/cl": VS_2017u8 + VS_PLATFORM_X86_64, + "/usr/bin/clang-cl": CLANG_CL_9_0 + CLANG_CL_PLATFORM_X86_64, + "/usr/bin/clang-cl-3.9": CLANG_CL_3_9 + CLANG_CL_PLATFORM_X86_64, + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/gcc-4.9": GCC_4_9 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++-4.9": GXX_4_9 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/gcc-5": GCC_5 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++-5": GXX_5 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/gcc-6": GCC_6 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++-6": GXX_6 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/gcc-7": GCC_7 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/g++-7": GXX_7 + GCC_PLATFORM_X86_64_WIN + MINGW32, + "/usr/bin/clang": DEFAULT_CLANG + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang++": DEFAULT_CLANGXX + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang-7.0": CLANG_7_0 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang++-7.0": CLANGXX_7_0 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang-4.0": CLANG_4_0 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang++-4.0": CLANGXX_4_0 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang-3.3": CLANG_3_3 + CLANG_PLATFORM_X86_64_WIN, + "/usr/bin/clang++-3.3": CLANGXX_3_3 + CLANG_PLATFORM_X86_64_WIN, + } + + +class WindowsToolchainTest(BaseToolchainTest): + HOST = "i686-pc-windows-msvc" + + PATHS = MingwToolchainTest.PATHS + + def test_unsupported_msvc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "/usr/bin/cl"}, + ) + + def test_unsupported_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": MingwToolchainTest.CLANG_CL_3_9_RESULT}, + environ={"CC": "/usr/bin/clang-cl-3.9"}, + ) + + def test_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": MingwToolchainTest.CLANG_CL_9_0_RESULT, + "cxx_compiler": MingwToolchainTest.CLANGXX_CL_9_0_RESULT, + }, + ) + + def test_unsupported_gcc(self): + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) != "clang-cl" + } + self.do_toolchain_test( + paths, + {"c_compiler": "Cannot find the target C compiler"}, + ) + + def test_overridden_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "gcc-5", "CXX": "g++-5"}, + ) + + def test_unsupported_clang(self): + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("clang-cl", "gcc") + } + self.do_toolchain_test( + paths, + {"c_compiler": "Cannot find the target C compiler"}, + ) + + def test_overridden_unsupported_clang(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + + +class Windows64ToolchainTest(WindowsToolchainTest): + HOST = "x86_64-pc-windows-msvc" + + PATHS = Mingw64ToolchainTest.PATHS + + +class WindowsGnuToolchainTest(BaseToolchainTest): + HOST = "i686-pc-windows-gnu" + + PATHS = MingwToolchainTest.PATHS + + def test_unsupported_msvc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "/usr/bin/cl"}, + ) + + def test_unsupported_clang_cl(self): + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) == "clang-cl" + } + self.do_toolchain_test( + paths, + {"c_compiler": "Cannot find the target C compiler"}, + ) + + def test_overridden_unsupported_clang_cl(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "clang-cl", "CXX": "clang-cl"}, + ) + + def test_unsupported_gcc(self): + paths = { + k: v for k, v in six.iteritems(self.PATHS) if os.path.basename(k) == "gcc" + } + self.do_toolchain_test( + paths, + {"c_compiler": "Cannot find the target C compiler"}, + ) + + def test_overridden_unsupported_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": "Unknown compiler or compiler not supported."}, + environ={"CC": "gcc-5", "CXX": "g++-5"}, + ) + + def test_clang(self): + paths = { + k: v + for k, v in six.iteritems(self.PATHS) + if os.path.basename(k) not in ("clang-cl", "gcc") + } + self.do_toolchain_test( + paths, + { + "c_compiler": MingwToolchainTest.DEFAULT_CLANG_RESULT, + "cxx_compiler": MingwToolchainTest.DEFAULT_CLANGXX_RESULT, + }, + ) + + def test_overridden_unsupported_clang(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": MingwToolchainTest.CLANG_3_3_RESULT, + "cxx_compiler": MingwToolchainTest.CLANGXX_3_3_RESULT, + }, + environ={"CC": "clang-3.3", "CXX": "clang++-3.3"}, + ) + + +class WindowsGnu64ToolchainTest(WindowsGnuToolchainTest): + HOST = "x86_64-pc-windows-gnu" + + PATHS = Mingw64ToolchainTest.PATHS + + +class LinuxCrossCompileToolchainTest(BaseToolchainTest): + TARGET = "arm-unknown-linux-gnu" + PATHS = { + "/usr/bin/arm-linux-gnu-gcc-4.9": GCC_4_9 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-g++-4.9": GXX_4_9 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-gcc-5": GCC_5 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-g++-5": GXX_5 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-gcc": DEFAULT_GCC + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-g++": DEFAULT_GXX + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-gcc-7": GCC_7 + GCC_PLATFORM_ARM_LINUX, + "/usr/bin/arm-linux-gnu-g++-7": GXX_7 + GCC_PLATFORM_ARM_LINUX, + } + PATHS.update(LinuxToolchainTest.PATHS) + ARM_GCC_4_9_RESULT = LinuxToolchainTest.GCC_4_9_RESULT + ARM_GXX_4_9_RESULT = LinuxToolchainTest.GXX_4_9_RESULT + ARM_GCC_5_RESULT = LinuxToolchainTest.GCC_5_RESULT + ARM_GXX_5_RESULT = LinuxToolchainTest.GXX_5_RESULT + ARM_DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + { + "compiler": "/usr/bin/arm-linux-gnu-gcc" + } + ARM_DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + { + "compiler": "/usr/bin/arm-linux-gnu-g++" + } + ARM_GCC_7_RESULT = LinuxToolchainTest.GCC_7_RESULT + ARM_GXX_7_RESULT = LinuxToolchainTest.GXX_7_RESULT + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + + little_endian = FakeCompiler(GCC_PLATFORM_LINUX, GCC_PLATFORM_LITTLE_ENDIAN) + big_endian = FakeCompiler(GCC_PLATFORM_LINUX, GCC_PLATFORM_BIG_ENDIAN) + + PLATFORMS = { + "i686-pc-linux-gnu": GCC_PLATFORM_X86_LINUX, + "x86_64-pc-linux-gnu": GCC_PLATFORM_X86_64_LINUX, + "arm-unknown-linux-gnu": GCC_PLATFORM_ARM_LINUX, + "aarch64-unknown-linux-gnu": little_endian + {"__aarch64__": 1}, + "ia64-unknown-linux-gnu": little_endian + {"__ia64__": 1}, + "s390x-unknown-linux-gnu": big_endian + {"__s390x__": 1, "__s390__": 1}, + "s390-unknown-linux-gnu": big_endian + {"__s390__": 1}, + "powerpc64-unknown-linux-gnu": big_endian + + { + None: {"__powerpc64__": 1, "__powerpc__": 1}, + "-m32": {"__powerpc64__": False}, + }, + "powerpc-unknown-linux-gnu": big_endian + + {None: {"__powerpc__": 1}, "-m64": {"__powerpc64__": 1}}, + "alpha-unknown-linux-gnu": little_endian + {"__alpha__": 1}, + "hppa-unknown-linux-gnu": big_endian + {"__hppa__": 1}, + "sparc64-unknown-linux-gnu": big_endian + + {None: {"__arch64__": 1, "__sparc__": 1}, "-m32": {"__arch64__": False}}, + "sparc-unknown-linux-gnu": big_endian + + {None: {"__sparc__": 1}, "-m64": {"__arch64__": 1}}, + "m68k-unknown-linux-gnu": big_endian + {"__m68k__": 1}, + "mips64-unknown-linux-gnuabi64": big_endian + {"__mips64": 1, "__mips__": 1}, + "mips-unknown-linux-gnu": big_endian + {"__mips__": 1}, + "riscv64-unknown-linux-gnu": little_endian + {"__riscv": 1, "__riscv_xlen": 64}, + "sh4-unknown-linux-gnu": little_endian + {"__sh__": 1}, + } + + PLATFORMS["powerpc64le-unknown-linux-gnu"] = ( + PLATFORMS["powerpc64-unknown-linux-gnu"] + GCC_PLATFORM_LITTLE_ENDIAN + ) + PLATFORMS["mips64el-unknown-linux-gnuabi64"] = ( + PLATFORMS["mips64-unknown-linux-gnuabi64"] + GCC_PLATFORM_LITTLE_ENDIAN + ) + PLATFORMS["mipsel-unknown-linux-gnu"] = ( + PLATFORMS["mips-unknown-linux-gnu"] + GCC_PLATFORM_LITTLE_ENDIAN + ) + + def do_test_cross_gcc_32_64(self, host, target): + self.HOST = host + self.TARGET = target + paths = { + "/usr/bin/gcc": DEFAULT_GCC + self.PLATFORMS[host], + "/usr/bin/g++": DEFAULT_GXX + self.PLATFORMS[host], + } + cross_flags = {"flags": ["-m64" if "64" in target else "-m32"]} + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_GCC_RESULT + cross_flags, + "cxx_compiler": self.DEFAULT_GXX_RESULT + cross_flags, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + ) + self.HOST = LinuxCrossCompileToolchainTest.HOST + self.TARGET = LinuxCrossCompileToolchainTest.TARGET + + def test_cross_x86_x64(self): + self.do_test_cross_gcc_32_64("i686-pc-linux-gnu", "x86_64-pc-linux-gnu") + self.do_test_cross_gcc_32_64("x86_64-pc-linux-gnu", "i686-pc-linux-gnu") + + def test_cross_sparc_sparc64(self): + self.do_test_cross_gcc_32_64( + "sparc-unknown-linux-gnu", "sparc64-unknown-linux-gnu" + ) + self.do_test_cross_gcc_32_64( + "sparc64-unknown-linux-gnu", "sparc-unknown-linux-gnu" + ) + + def test_cross_ppc_ppc64(self): + self.do_test_cross_gcc_32_64( + "powerpc-unknown-linux-gnu", "powerpc64-unknown-linux-gnu" + ) + self.do_test_cross_gcc_32_64( + "powerpc64-unknown-linux-gnu", "powerpc-unknown-linux-gnu" + ) + + def do_test_cross_gcc(self, host, target): + self.HOST = host + self.TARGET = target + host_cpu = host.split("-")[0] + cpu, manufacturer, os = target.split("-", 2) + toolchain_prefix = "/usr/bin/%s-%s" % (cpu, os) + paths = { + "/usr/bin/gcc": DEFAULT_GCC + self.PLATFORMS[host], + "/usr/bin/g++": DEFAULT_GXX + self.PLATFORMS[host], + } + self.do_toolchain_test( + paths, + { + "c_compiler": ( + "Target C compiler target CPU (%s) " + "does not match --target CPU (%s)" % (host_cpu, cpu) + ) + }, + ) + + paths.update( + { + "%s-gcc" % toolchain_prefix: DEFAULT_GCC + self.PLATFORMS[target], + "%s-g++" % toolchain_prefix: DEFAULT_GXX + self.PLATFORMS[target], + } + ) + self.do_toolchain_test( + paths, + { + "c_compiler": self.DEFAULT_GCC_RESULT + + {"compiler": "%s-gcc" % toolchain_prefix}, + "cxx_compiler": self.DEFAULT_GXX_RESULT + + {"compiler": "%s-g++" % toolchain_prefix}, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + ) + self.HOST = LinuxCrossCompileToolchainTest.HOST + self.TARGET = LinuxCrossCompileToolchainTest.TARGET + + def test_cross_gcc_misc(self): + for target in self.PLATFORMS: + if not target.endswith("-pc-linux-gnu"): + self.do_test_cross_gcc("x86_64-pc-linux-gnu", target) + + def test_cannot_cross(self): + self.TARGET = "mipsel-unknown-linux-gnu" + + paths = { + "/usr/bin/gcc": DEFAULT_GCC + self.PLATFORMS["mips-unknown-linux-gnu"], + "/usr/bin/g++": DEFAULT_GXX + self.PLATFORMS["mips-unknown-linux-gnu"], + } + self.do_toolchain_test( + paths, + { + "c_compiler": ( + "Target C compiler target endianness (big) " + "does not match --target endianness (little)" + ) + }, + ) + self.TARGET = LinuxCrossCompileToolchainTest.TARGET + + def test_overridden_cross_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.ARM_GCC_7_RESULT, + "cxx_compiler": self.ARM_GXX_7_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "arm-linux-gnu-gcc-7", "CXX": "arm-linux-gnu-g++-7"}, + ) + + def test_overridden_unsupported_cross_gcc(self): + self.do_toolchain_test( + self.PATHS, + {"c_compiler": self.ARM_GCC_4_9_RESULT}, + environ={"CC": "arm-linux-gnu-gcc-4.9", "CXX": "arm-linux-gnu-g++-4.9"}, + ) + + def test_guess_cross_cxx(self): + # When CXX is not set, we guess it from CC. + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.ARM_GCC_7_RESULT, + "cxx_compiler": self.ARM_GXX_7_RESULT, + "host_c_compiler": self.DEFAULT_GCC_RESULT, + "host_cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + environ={"CC": "arm-linux-gnu-gcc-7"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.ARM_DEFAULT_GCC_RESULT, + "cxx_compiler": self.ARM_DEFAULT_GXX_RESULT, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={"CC": "arm-linux-gnu-gcc", "HOST_CC": "clang"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.ARM_DEFAULT_GCC_RESULT, + "cxx_compiler": self.ARM_DEFAULT_GXX_RESULT, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={ + "CC": "arm-linux-gnu-gcc", + "CXX": "arm-linux-gnu-g++", + "HOST_CC": "clang", + }, + ) + + def test_cross_clang(self): + cross_clang_result = self.DEFAULT_CLANG_RESULT + { + "flags": ["--target=arm-linux-gnu"] + } + cross_clangxx_result = self.DEFAULT_CLANGXX_RESULT + { + "flags": ["--target=arm-linux-gnu"] + } + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": cross_clang_result, + "cxx_compiler": cross_clangxx_result, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={"CC": "clang", "HOST_CC": "clang"}, + ) + + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": cross_clang_result, + "cxx_compiler": cross_clangxx_result, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={"CC": "clang"}, + ) + + def test_cross_atypical_clang(self): + paths = dict(self.PATHS) + paths.update( + { + "/usr/bin/afl-clang-fast": paths["/usr/bin/clang"], + "/usr/bin/afl-clang-fast++": paths["/usr/bin/clang++"], + } + ) + afl_clang_result = self.DEFAULT_CLANG_RESULT + { + "compiler": "/usr/bin/afl-clang-fast" + } + afl_clangxx_result = self.DEFAULT_CLANGXX_RESULT + { + "compiler": "/usr/bin/afl-clang-fast++" + } + self.do_toolchain_test( + paths, + { + "c_compiler": afl_clang_result + {"flags": ["--target=arm-linux-gnu"]}, + "cxx_compiler": afl_clangxx_result + + {"flags": ["--target=arm-linux-gnu"]}, + "host_c_compiler": afl_clang_result, + "host_cxx_compiler": afl_clangxx_result, + }, + environ={"CC": "afl-clang-fast", "CXX": "afl-clang-fast++"}, + ) + + +class OSXCrossToolchainTest(BaseToolchainTest): + TARGET = "i686-apple-darwin11.2.0" + PATHS = dict(LinuxToolchainTest.PATHS) + PATHS.update( + { + "/usr/bin/clang": CLANG_7_0 + CLANG_PLATFORM_X86_64_LINUX, + "/usr/bin/clang++": CLANGXX_7_0 + CLANG_PLATFORM_X86_64_LINUX, + } + ) + DEFAULT_CLANG_RESULT = CompilerResult( + flags=["-std=gnu99"], + version="7.0.0", + type="clang", + compiler="/usr/bin/clang", + language="C", + ) + DEFAULT_CLANGXX_RESULT = CompilerResult( + flags=["-std=gnu++17"], + version="7.0.0", + type="clang", + compiler="/usr/bin/clang++", + language="C++", + ) + + def test_osx_cross(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_CLANG_RESULT + + OSXToolchainTest.SYSROOT_FLAGS + + {"flags": ["--target=i686-apple-darwin11.2.0"]}, + "cxx_compiler": self.DEFAULT_CLANGXX_RESULT + + {"flags": PrependFlags(["-stdlib=libc++"])} + + OSXToolchainTest.SYSROOT_FLAGS + + {"flags": ["--target=i686-apple-darwin11.2.0"]}, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + environ={"CC": "clang"}, + args=["--with-macos-sdk=%s" % OSXToolchainTest.SYSROOT_FLAGS["flags"][1]], + ) + + def test_cannot_osx_cross(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": "Target C compiler target kernel (Linux) does not " + "match --target kernel (Darwin)" + }, + environ={"CC": "gcc"}, + args=["--with-macos-sdk=%s" % OSXToolchainTest.SYSROOT_FLAGS["flags"][1]], + ) + + +class WindowsCrossToolchainTest(BaseToolchainTest): + TARGET = "x86_64-pc-windows-msvc" + DEFAULT_CLANG_RESULT = LinuxToolchainTest.DEFAULT_CLANG_RESULT + DEFAULT_CLANGXX_RESULT = LinuxToolchainTest.DEFAULT_CLANGXX_RESULT + + def test_clang_cl_cross(self): + paths = {"/usr/bin/clang-cl": CLANG_CL_9_0 + CLANG_CL_PLATFORM_X86_64} + paths.update(LinuxToolchainTest.PATHS) + self.do_toolchain_test( + paths, + { + "c_compiler": MingwToolchainTest.CLANG_CL_9_0_RESULT, + "cxx_compiler": MingwToolchainTest.CLANGXX_CL_9_0_RESULT, + "host_c_compiler": self.DEFAULT_CLANG_RESULT, + "host_cxx_compiler": self.DEFAULT_CLANGXX_RESULT, + }, + ) + + +class OpenBSDToolchainTest(BaseToolchainTest): + HOST = "x86_64-unknown-openbsd6.1" + TARGET = "x86_64-unknown-openbsd6.1" + PATHS = { + "/usr/bin/gcc": DEFAULT_GCC + GCC_PLATFORM_X86_64 + GCC_PLATFORM_OPENBSD, + "/usr/bin/g++": DEFAULT_GXX + GCC_PLATFORM_X86_64 + GCC_PLATFORM_OPENBSD, + } + DEFAULT_GCC_RESULT = LinuxToolchainTest.DEFAULT_GCC_RESULT + DEFAULT_GXX_RESULT = LinuxToolchainTest.DEFAULT_GXX_RESULT + + def test_gcc(self): + self.do_toolchain_test( + self.PATHS, + { + "c_compiler": self.DEFAULT_GCC_RESULT, + "cxx_compiler": self.DEFAULT_GXX_RESULT, + }, + ) + + +@memoize +def gen_invoke_cargo(version, rustup_wrapper=False): + def invoke_cargo(stdin, args): + args = tuple(args) + if not rustup_wrapper and args == ("+stable",): + return (101, "", "we are the real thing") + if args == ("--version", "--verbose"): + return 0, "cargo %s\nrelease: %s" % (version, version), "" + raise NotImplementedError("unsupported arguments") + + return invoke_cargo + + +@memoize +def gen_invoke_rustc(version, rustup_wrapper=False): + def invoke_rustc(stdin, args): + args = tuple(args) + # TODO: we don't have enough machinery set up to test the `rustup which` + # fallback yet. + if not rustup_wrapper and args == ("+stable",): + return (1, "", "error: couldn't read +stable: No such file or directory") + if args == ("--version", "--verbose"): + return ( + 0, + "rustc %s\nrelease: %s\nhost: x86_64-unknown-linux-gnu" + % (version, version), + "", + ) + if args == ("--print", "target-list"): + # Raw list returned by rustc version 1.32, + ios, which somehow + # don't appear in the default list. + # https://github.com/rust-lang/rust/issues/36156 + rust_targets = [ + "aarch64-apple-ios", + "aarch64-fuchsia", + "aarch64-linux-android", + "aarch64-pc-windows-msvc", + "aarch64-unknown-cloudabi", + "aarch64-unknown-freebsd", + "aarch64-unknown-hermit", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "aarch64-unknown-netbsd", + "aarch64-unknown-none", + "aarch64-unknown-openbsd", + "arm-linux-androideabi", + "arm-unknown-linux-gnueabi", + "arm-unknown-linux-gnueabihf", + "arm-unknown-linux-musleabi", + "arm-unknown-linux-musleabihf", + "armebv7r-none-eabi", + "armebv7r-none-eabihf", + "armv4t-unknown-linux-gnueabi", + "armv5te-unknown-linux-gnueabi", + "armv5te-unknown-linux-musleabi", + "armv6-unknown-netbsd-eabihf", + "armv7-linux-androideabi", + "armv7-unknown-cloudabi-eabihf", + "armv7-unknown-linux-gnueabihf", + "armv7-unknown-linux-musleabihf", + "armv7-unknown-netbsd-eabihf", + "armv7r-none-eabi", + "armv7r-none-eabihf", + "armv7s-apple-ios", + "asmjs-unknown-emscripten", + "i386-apple-ios", + "i586-pc-windows-msvc", + "i586-unknown-linux-gnu", + "i586-unknown-linux-musl", + "i686-apple-darwin", + "i686-linux-android", + "i686-pc-windows-gnu", + "i686-pc-windows-msvc", + "i686-unknown-cloudabi", + "i686-unknown-dragonfly", + "i686-unknown-freebsd", + "i686-unknown-haiku", + "i686-unknown-linux-gnu", + "i686-unknown-linux-musl", + "i686-unknown-netbsd", + "i686-unknown-openbsd", + "mips-unknown-linux-gnu", + "mips-unknown-linux-musl", + "mips-unknown-linux-uclibc", + "mips64-unknown-linux-gnuabi64", + "mips64el-unknown-linux-gnuabi64", + "mipsel-unknown-linux-gnu", + "mipsel-unknown-linux-musl", + "mipsel-unknown-linux-uclibc", + "msp430-none-elf", + "powerpc-unknown-linux-gnu", + "powerpc-unknown-linux-gnuspe", + "powerpc-unknown-linux-musl", + "powerpc-unknown-netbsd", + "powerpc64-unknown-linux-gnu", + "powerpc64-unknown-linux-musl", + "powerpc64le-unknown-linux-gnu", + "powerpc64le-unknown-linux-musl", + "riscv32imac-unknown-none-elf", + "riscv32imc-unknown-none-elf", + "s390x-unknown-linux-gnu", + "sparc-unknown-linux-gnu", + "sparc64-unknown-linux-gnu", + "sparc64-unknown-netbsd", + "sparcv9-sun-solaris", + "thumbv6m-none-eabi", + "thumbv7a-pc-windows-msvc", + "thumbv7em-none-eabi", + "thumbv7em-none-eabihf", + "thumbv7m-none-eabi", + "thumbv8m.base-none-eabi", + "wasm32-experimental-emscripten", + "wasm32-unknown-emscripten", + "wasm32-unknown-unknown", + "x86_64-apple-darwin", + "x86_64-apple-ios", + "x86_64-fortanix-unknown-sgx", + "x86_64-fuchsia", + "x86_64-linux-android", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-msvc", + "x86_64-rumprun-netbsd", + "x86_64-sun-solaris", + "x86_64-unknown-bitrig", + "x86_64-unknown-cloudabi", + "x86_64-unknown-dragonfly", + "x86_64-unknown-freebsd", + "x86_64-unknown-haiku", + "x86_64-unknown-hermit", + "x86_64-unknown-l4re-uclibc", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-gnux32", + "x86_64-unknown-linux-musl", + "x86_64-unknown-netbsd", + "x86_64-unknown-openbsd", + "x86_64-unknown-redox", + ] + # Additional targets from 1.33 + if Version(version) >= "1.33.0": + rust_targets += [ + "thumbv7neon-linux-androideabi", + "thumbv7neon-unknown-linux-gnueabihf", + "x86_64-unknown-uefi", + "thumbv8m.main-none-eabi", + "thumbv8m.main-none-eabihf", + ] + # Additional targets from 1.34 + if Version(version) >= "1.34.0": + rust_targets += [ + "nvptx64-nvidia-cuda", + "powerpc64-unknown-freebsd", + "riscv64gc-unknown-none-elf", + "riscv64imac-unknown-none-elf", + ] + # Additional targets from 1.35 + if Version(version) >= "1.35.0": + rust_targets += [ + "armv6-unknown-freebsd", + "armv7-unknown-freebsd", + "mipsisa32r6-unknown-linux-gnu", + "mipsisa32r6el-unknown-linux-gnu", + "mipsisa64r6-unknown-linux-gnuabi64", + "mipsisa64r6el-unknown-linux-gnuabi64", + "wasm32-unknown-wasi", + ] + # Additional targets from 1.36 + if Version(version) >= "1.36.0": + rust_targets += ["wasm32-wasi"] + rust_targets.remove("wasm32-unknown-wasi") + rust_targets.remove("x86_64-unknown-bitrig") + # Additional targets from 1.37 + if Version(version) >= "1.37.0": + rust_targets += ["x86_64-pc-solaris"] + # Additional targets from 1.38 + if Version(version) >= "1.38.0": + rust_targets += [ + "aarch64-unknown-redox", + "aarch64-wrs-vxworks", + "armv7-unknown-linux-gnueabi", + "armv7-unknown-linux-musleabi", + "armv7-wrs-vxworks", + "hexagon-unknown-linux-musl", + "i586-wrs-vxworks", + "i686-uwp-windows-gnu", + "i686-wrs-vxworks", + "powerpc-wrs-vxworks", + "powerpc-wrs-vxworks-spe", + "powerpc64-wrs-vxworks", + "riscv32i-unknown-none-elf", + "x86_64-uwp-windows-gnu", + "x86_64-wrs-vxworks", + ] + # Additional targets from 1.38 + if Version(version) >= "1.39.0": + rust_targets += [ + "aarch64-uwp-windows-msvc", + "armv7-wrs-vxworks-eabihf", + "i686-unknown-uefi", + "i686-uwp-windows-msvc", + "mips64-unknown-linux-muslabi64", + "mips64el-unknown-linux-muslabi64", + "sparc64-unknown-openbsd", + "x86_64-linux-kernel", + "x86_64-uwp-windows-msvc", + ] + rust_targets.remove("armv7-wrs-vxworks") + rust_targets.remove("i586-wrs-vxworks") + + return 0, "\n".join(sorted(rust_targets)), "" + if ( + len(args) == 6 + and args[:2] == ("--crate-type", "staticlib") + and args[2].startswith("--target=") + and args[3] == "-o" + ): + with open(args[4], "w") as fh: + fh.write("foo") + return 0, "", "" + raise NotImplementedError("unsupported arguments") + + return invoke_rustc + + +class RustTest(BaseConfigureTest): + def get_rust_target( + self, target, compiler_type="gcc", version=MINIMUM_RUST_VERSION, arm_target=None + ): + environ = { + "PATH": os.pathsep.join(mozpath.abspath(p) for p in ("/bin", "/usr/bin")) + } + + paths = { + mozpath.abspath("/usr/bin/cargo"): gen_invoke_cargo(version), + mozpath.abspath("/usr/bin/rustc"): gen_invoke_rustc(version), + } + + self.TARGET = target + sandbox = self.get_sandbox(paths, {}, [], environ) + + # Trick the sandbox into not running the target compiler check + dep = sandbox._depends[sandbox["c_compiler"]] + getattr(sandbox, "__value_for_depends")[(dep,)] = CompilerResult( + type=compiler_type + ) + # Same for the arm_target checks. + dep = sandbox._depends[sandbox["arm_target"]] + getattr(sandbox, "__value_for_depends")[ + (dep,) + ] = arm_target or ReadOnlyNamespace( + arm_arch=7, thumb2=False, fpu="vfpv2", float_abi="softfp" + ) + return sandbox._value_for(sandbox["rust_target_triple"]) + + def test_rust_target(self): + # Cases where the output of config.sub matches a rust target + for straightforward in ( + "x86_64-unknown-dragonfly", + "aarch64-unknown-freebsd", + "i686-unknown-freebsd", + "x86_64-unknown-freebsd", + "sparc64-unknown-netbsd", + "i686-unknown-netbsd", + "x86_64-unknown-netbsd", + "i686-unknown-openbsd", + "x86_64-unknown-openbsd", + "aarch64-unknown-linux-gnu", + "sparc64-unknown-linux-gnu", + "i686-unknown-linux-gnu", + "i686-apple-darwin", + "x86_64-apple-darwin", + "mips-unknown-linux-gnu", + "mipsel-unknown-linux-gnu", + "mips64-unknown-linux-gnuabi64", + "mips64el-unknown-linux-gnuabi64", + "powerpc64-unknown-linux-gnu", + "powerpc64le-unknown-linux-gnu", + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", + "i686-pc-windows-gnu", + "x86_64-pc-windows-gnu", + ): + self.assertEqual(self.get_rust_target(straightforward), straightforward) + + # Cases where the output of config.sub is different + for autoconf, rust in ( + ("aarch64-unknown-linux-android", "aarch64-linux-android"), + ("arm-unknown-linux-androideabi", "armv7-linux-androideabi"), + ("armv7-unknown-linux-androideabi", "armv7-linux-androideabi"), + ("i386-unknown-linux-android", "i686-linux-android"), + ("i686-unknown-linux-android", "i686-linux-android"), + ("i686-pc-linux-gnu", "i686-unknown-linux-gnu"), + ("x86_64-unknown-linux-android", "x86_64-linux-android"), + ("x86_64-pc-linux-gnu", "x86_64-unknown-linux-gnu"), + ("sparcv9-sun-solaris2", "sparcv9-sun-solaris"), + ("x86_64-sun-solaris2", "x86_64-sun-solaris"), + ): + self.assertEqual(self.get_rust_target(autoconf), rust) + + # Windows + for autoconf, building_with_gcc, rust in ( + ("i686-pc-mingw32", "clang-cl", "i686-pc-windows-msvc"), + ("x86_64-pc-mingw32", "clang-cl", "x86_64-pc-windows-msvc"), + ("i686-pc-mingw32", "clang", "i686-pc-windows-gnu"), + ("x86_64-pc-mingw32", "clang", "x86_64-pc-windows-gnu"), + ("i686-w64-mingw32", "clang", "i686-pc-windows-gnu"), + ("x86_64-w64-mingw32", "clang", "x86_64-pc-windows-gnu"), + ("aarch64-windows-mingw32", "clang-cl", "aarch64-pc-windows-msvc"), + ): + self.assertEqual(self.get_rust_target(autoconf, building_with_gcc), rust) + + # Arm special cases + self.assertEqual( + self.get_rust_target( + "arm-unknown-linux-androideabi", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="neon", thumb2=True, float_abi="softfp" + ), + ), + "thumbv7neon-linux-androideabi", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-linux-androideabi", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="neon", thumb2=False, float_abi="softfp" + ), + ), + "armv7-linux-androideabi", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-linux-androideabi", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="vfpv2", thumb2=True, float_abi="softfp" + ), + ), + "armv7-linux-androideabi", + ) + + self.assertEqual( + self.get_rust_target( + "armv7-unknown-linux-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="neon", thumb2=True, float_abi="hard" + ), + ), + "thumbv7neon-unknown-linux-gnueabihf", + ) + + self.assertEqual( + self.get_rust_target( + "armv7-unknown-linux-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="neon", thumb2=False, float_abi="hard" + ), + ), + "armv7-unknown-linux-gnueabihf", + ) + + self.assertEqual( + self.get_rust_target( + "armv7-unknown-linux-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="vfpv2", thumb2=True, float_abi="hard" + ), + ), + "armv7-unknown-linux-gnueabihf", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-freebsd13.0-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=7, fpu="vfpv2", thumb2=True, float_abi="hard" + ), + ), + "armv7-unknown-freebsd", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-freebsd13.0-gnueabihf", + arm_target=ReadOnlyNamespace( + arm_arch=6, fpu=None, thumb2=False, float_abi="hard" + ), + ), + "armv6-unknown-freebsd", + ) + + self.assertEqual( + self.get_rust_target( + "arm-unknown-linux-gnueabi", + arm_target=ReadOnlyNamespace( + arm_arch=4, fpu=None, thumb2=False, float_abi="softfp" + ), + ), + "armv4t-unknown-linux-gnueabi", + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py b/python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py new file mode 100644 index 0000000000..f42778215b --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py @@ -0,0 +1,433 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import copy +import re +import unittest +from fnmatch import fnmatch +from textwrap import dedent + +import six +from mozpack import path as mozpath +from mozunit import MockedOpen, main +from six import StringIO + +from mozbuild.preprocessor import Preprocessor +from mozbuild.util import ReadOnlyNamespace + + +class CompilerPreprocessor(Preprocessor): + # The C preprocessor only expands macros when they are not in C strings. + # For now, we don't look very hard for C strings because they don't matter + # that much for our unit tests, but we at least avoid expanding in the + # simple "FOO" case. + VARSUBST = re.compile('(?<!")(?P<VAR>\w+)(?!")', re.U) + NON_WHITESPACE = re.compile("\S") + HAS_FEATURE_OR_BUILTIN = re.compile( + '(__has_(?:feature|builtin|attribute|warning))\("?([^"\)]*)"?\)' + ) + + def __init__(self, *args, **kwargs): + Preprocessor.__init__(self, *args, **kwargs) + self.do_filter("c_substitution") + self.setMarker("#\s*") + + def do_if(self, expression, **kwargs): + # The C preprocessor handles numbers following C rules, which is a + # different handling than what our Preprocessor does out of the box. + # Hack around it enough that the configure tests work properly. + context = self.context + + def normalize_numbers(value): + if isinstance(value, six.string_types): + if value[-1:] == "L" and value[:-1].isdigit(): + value = int(value[:-1]) + return value + + # Our Preprocessor doesn't handle macros with parameters, so we hack + # around that for __has_feature()-like things. + + def normalize_has_feature_or_builtin(expr): + return ( + self.HAS_FEATURE_OR_BUILTIN.sub(r"\1\2", expr) + .replace("-", "_") + .replace("+", "_") + ) + + self.context = self.Context( + (normalize_has_feature_or_builtin(k), normalize_numbers(v)) + for k, v in six.iteritems(context) + ) + try: + return Preprocessor.do_if( + self, normalize_has_feature_or_builtin(expression), **kwargs + ) + finally: + self.context = context + + class Context(dict): + def __missing__(self, key): + return None + + def filter_c_substitution(self, line): + def repl(matchobj): + varname = matchobj.group("VAR") + if varname in self.context: + result = six.text_type(self.context[varname]) + # The C preprocessor inserts whitespaces around expanded + # symbols. + start, end = matchobj.span("VAR") + if self.NON_WHITESPACE.match(line[start - 1 : start]): + result = " " + result + if self.NON_WHITESPACE.match(line[end : end + 1]): + result = result + " " + return result + return matchobj.group(0) + + return self.VARSUBST.sub(repl, line) + + +class TestCompilerPreprocessor(unittest.TestCase): + def test_expansion(self): + pp = CompilerPreprocessor({"A": 1, "B": "2", "C": "c", "D": "d"}) + pp.out = StringIO() + input = StringIO('A.B.C "D"') + input.name = "foo" + pp.do_include(input) + + self.assertEqual(pp.out.getvalue(), '1 . 2 . c "D"') + + def test_normalization(self): + pp = CompilerPreprocessor( + {"__has_attribute(bar)": 1, '__has_warning("-Wc++98-foo")': 1} + ) + pp.out = StringIO() + input = StringIO( + dedent( + """\ + #if __has_warning("-Wbar") + WBAR + #endif + #if __has_warning("-Wc++98-foo") + WFOO + #endif + #if !__has_warning("-Wc++98-foo") + NO_WFOO + #endif + #if __has_attribute(bar) + BAR + #else + NO_BAR + #endif + #if !__has_attribute(foo) + NO_FOO + #endif + """ + ) + ) + + input.name = "foo" + pp.do_include(input) + + self.assertEqual(pp.out.getvalue(), "WFOO\nBAR\nNO_FOO\n") + + def test_condition(self): + pp = CompilerPreprocessor({"A": 1, "B": "2", "C": "0L"}) + pp.out = StringIO() + input = StringIO( + dedent( + """\ + #ifdef A + IFDEF_A + #endif + #if A + IF_A + #endif + # if B + IF_B + # else + IF_NOT_B + # endif + #if !C + IF_NOT_C + #else + IF_C + #endif + """ + ) + ) + input.name = "foo" + pp.do_include(input) + + self.assertEqual("IFDEF_A\nIF_A\nIF_NOT_B\nIF_NOT_C\n", pp.out.getvalue()) + + +class FakeCompiler(dict): + """Defines a fake compiler for use in toolchain tests below. + + The definitions given when creating an instance can have one of two + forms: + - a dict giving preprocessor symbols and their respective value, e.g. + { '__GNUC__': 4, '__STDC__': 1 } + - a dict associating flags to preprocessor symbols. An entry for `None` + is required in this case. Those are the baseline preprocessor symbols. + Additional entries describe additional flags to set or existing flags + to unset (with a value of `False`). + { + None: { '__GNUC__': 4, '__STDC__': 1, '__STRICT_ANSI__': 1 }, + '-std=gnu99': { '__STDC_VERSION__': '199901L', + '__STRICT_ANSI__': False }, + } + With the dict above, invoking the preprocessor with no additional flags + would define __GNUC__, __STDC__ and __STRICT_ANSI__, and with -std=gnu99, + __GNUC__, __STDC__, and __STDC_VERSION__ (__STRICT_ANSI__ would be + unset). + It is also possible to have different symbols depending on the source + file extension. In this case, the key is '*.ext'. e.g. + { + '*.c': { '__STDC__': 1 }, + '*.cpp': { '__cplusplus': '199711L' }, + } + + All the given definitions are merged together. + + A FakeCompiler instance itself can be used as a definition to create + another FakeCompiler. + + For convenience, FakeCompiler instances can be added (+) to one another. + """ + + def __init__(self, *definitions): + for definition in definitions: + if all(not isinstance(d, dict) for d in six.itervalues(definition)): + definition = {None: definition} + for key, value in six.iteritems(definition): + self.setdefault(key, {}).update(value) + + def __call__(self, stdin, args): + files = [] + flags = [] + args = iter(args) + while True: + arg = next(args, None) + if arg is None: + break + if arg.startswith("-"): + # Ignore -isysroot/--sysroot and the argument that follows it. + if arg in ("-isysroot", "--sysroot"): + next(args, None) + else: + flags.append(arg) + else: + files.append(arg) + + if "-E" in flags: + assert len(files) == 1 + file = files[0] + pp = CompilerPreprocessor(self[None]) + + def apply_defn(defn): + for k, v in six.iteritems(defn): + if v is False: + if k in pp.context: + del pp.context[k] + else: + pp.context[k] = v + + for glob, defn in six.iteritems(self): + if glob and not glob.startswith("-") and fnmatch(file, glob): + apply_defn(defn) + + for flag in flags: + apply_defn(self.get(flag, {})) + + pp.out = StringIO() + pp.do_include(file) + return 0, pp.out.getvalue(), "" + elif "-c" in flags: + if "-funknown-flag" in flags: + return 1, "", "" + return 0, "", "" + + return 1, "", "" + + def __add__(self, other): + return FakeCompiler(self, other) + + +class TestFakeCompiler(unittest.TestCase): + def test_fake_compiler(self): + with MockedOpen({"file": "A B C", "file.c": "A B C"}): + compiler = FakeCompiler({"A": "1", "B": "2"}) + self.assertEqual(compiler(None, ["-E", "file"]), (0, "1 2 C", "")) + + compiler = FakeCompiler( + { + None: {"A": "1", "B": "2"}, + "-foo": {"C": "foo"}, + "-bar": {"B": "bar", "C": "bar"}, + "-qux": {"B": False}, + "*.c": {"B": "42"}, + } + ) + self.assertEqual(compiler(None, ["-E", "file"]), (0, "1 2 C", "")) + self.assertEqual(compiler(None, ["-E", "-foo", "file"]), (0, "1 2 foo", "")) + self.assertEqual( + compiler(None, ["-E", "-bar", "file"]), (0, "1 bar bar", "") + ) + self.assertEqual(compiler(None, ["-E", "-qux", "file"]), (0, "1 B C", "")) + self.assertEqual( + compiler(None, ["-E", "-foo", "-bar", "file"]), (0, "1 bar bar", "") + ) + self.assertEqual( + compiler(None, ["-E", "-bar", "-foo", "file"]), (0, "1 bar foo", "") + ) + self.assertEqual( + compiler(None, ["-E", "-bar", "-qux", "file"]), (0, "1 B bar", "") + ) + self.assertEqual( + compiler(None, ["-E", "-qux", "-bar", "file"]), (0, "1 bar bar", "") + ) + self.assertEqual(compiler(None, ["-E", "file.c"]), (0, "1 42 C", "")) + self.assertEqual( + compiler(None, ["-E", "-bar", "file.c"]), (0, "1 bar bar", "") + ) + + def test_multiple_definitions(self): + compiler = FakeCompiler({"A": 1, "B": 2}, {"C": 3}) + + self.assertEqual(compiler, {None: {"A": 1, "B": 2, "C": 3}}) + compiler = FakeCompiler({"A": 1, "B": 2}, {"B": 4, "C": 3}) + + self.assertEqual(compiler, {None: {"A": 1, "B": 4, "C": 3}}) + compiler = FakeCompiler( + {"A": 1, "B": 2}, {None: {"B": 4, "C": 3}, "-foo": {"D": 5}} + ) + + self.assertEqual(compiler, {None: {"A": 1, "B": 4, "C": 3}, "-foo": {"D": 5}}) + + compiler = FakeCompiler( + {None: {"A": 1, "B": 2}, "-foo": {"D": 5}}, + {"-foo": {"D": 5}, "-bar": {"E": 6}}, + ) + + self.assertEqual( + compiler, {None: {"A": 1, "B": 2}, "-foo": {"D": 5}, "-bar": {"E": 6}} + ) + + +class PrependFlags(list): + """Wrapper to allow to Prepend to flags instead of appending, in + CompilerResult. + """ + + +class CompilerResult(ReadOnlyNamespace): + """Helper of convenience to manipulate toolchain results in unit tests + + When adding a dict, the result is a new CompilerResult with the values + from the dict replacing those from the CompilerResult, except for `flags`, + where the value from the dict extends the `flags` in `self`. + """ + + def __init__( + self, wrapper=None, compiler="", version="", type="", language="", flags=None + ): + if flags is None: + flags = [] + if wrapper is None: + wrapper = [] + super(CompilerResult, self).__init__( + flags=flags, + version=version, + type=type, + compiler=mozpath.abspath(compiler), + wrapper=wrapper, + language=language, + ) + + def __add__(self, other): + assert isinstance(other, dict) + result = copy.deepcopy(self.__dict__) + for k, v in six.iteritems(other): + if k == "flags": + flags = result.setdefault(k, []) + if isinstance(v, PrependFlags): + flags[:0] = v + else: + flags.extend(v) + else: + result[k] = v + return CompilerResult(**result) + + +class TestCompilerResult(unittest.TestCase): + def test_compiler_result(self): + result = CompilerResult() + self.assertEqual( + result.__dict__, + { + "wrapper": [], + "compiler": mozpath.abspath(""), + "version": "", + "type": "", + "language": "", + "flags": [], + }, + ) + + result = CompilerResult( + compiler="/usr/bin/gcc", + version="4.2.1", + type="gcc", + language="C", + flags=["-std=gnu99"], + ) + self.assertEqual( + result.__dict__, + { + "wrapper": [], + "compiler": mozpath.abspath("/usr/bin/gcc"), + "version": "4.2.1", + "type": "gcc", + "language": "C", + "flags": ["-std=gnu99"], + }, + ) + + result2 = result + {"flags": ["-m32"]} + self.assertEqual( + result2.__dict__, + { + "wrapper": [], + "compiler": mozpath.abspath("/usr/bin/gcc"), + "version": "4.2.1", + "type": "gcc", + "language": "C", + "flags": ["-std=gnu99", "-m32"], + }, + ) + # Original flags are untouched. + self.assertEqual(result.flags, ["-std=gnu99"]) + + result3 = result + { + "compiler": "/usr/bin/gcc-4.7", + "version": "4.7.3", + "flags": ["-m32"], + } + self.assertEqual( + result3.__dict__, + { + "wrapper": [], + "compiler": mozpath.abspath("/usr/bin/gcc-4.7"), + "version": "4.7.3", + "type": "gcc", + "language": "C", + "flags": ["-std=gnu99", "-m32"], + }, + ) + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_toolkit_moz_configure.py b/python/mozbuild/mozbuild/test/configure/test_toolkit_moz_configure.py new file mode 100644 index 0000000000..e6b96b3627 --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_toolkit_moz_configure.py @@ -0,0 +1,102 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os + +from buildconfig import topsrcdir +from mozpack import path as mozpath +from mozunit import MockedOpen, main + +from common import BaseConfigureTest +from mozbuild.configure.options import InvalidOptionError + + +class TestToolkitMozConfigure(BaseConfigureTest): + def test_moz_configure_options(self): + def get_value_for(args=[], environ={}, mozconfig=""): + sandbox = self.get_sandbox({}, {}, args, environ, mozconfig) + + # Add a fake old-configure option + sandbox.option_impl( + "--with-foo", nargs="*", help="Help missing for old configure options" + ) + + # Remove all implied options, otherwise, getting + # all_configure_options below triggers them, and that triggers + # configure parts that aren't expected to run during this test. + del sandbox._implied_options[:] + result = sandbox._value_for(sandbox["all_configure_options"]) + shell = mozpath.abspath("/bin/sh") + return result.replace("CONFIG_SHELL=%s " % shell, "") + + self.assertEqual( + "--enable-application=browser", + get_value_for(["--enable-application=browser"]), + ) + + self.assertEqual( + "--enable-application=browser " "MOZ_VTUNE=1", + get_value_for(["--enable-application=browser", "MOZ_VTUNE=1"]), + ) + + value = get_value_for( + environ={"MOZ_VTUNE": "1"}, + mozconfig="ac_add_options --enable-application=browser", + ) + + self.assertEqual("--enable-application=browser MOZ_VTUNE=1", value) + + # --disable-js-shell is the default, so it's filtered out. + self.assertEqual( + "--enable-application=browser", + get_value_for(["--enable-application=browser", "--disable-js-shell"]), + ) + + # Normally, --without-foo would be filtered out because that's the + # default, but since it is a (fake) old-configure option, it always + # appears. + self.assertEqual( + "--enable-application=browser --without-foo", + get_value_for(["--enable-application=browser", "--without-foo"]), + ) + self.assertEqual( + "--enable-application=browser --with-foo", + get_value_for(["--enable-application=browser", "--with-foo"]), + ) + + self.assertEqual( + "--enable-application=browser '--with-foo=foo bar'", + get_value_for(["--enable-application=browser", "--with-foo=foo bar"]), + ) + + def test_developer_options(self, milestone="42.0a1"): + def get_value(args=[], environ={}): + sandbox = self.get_sandbox({}, {}, args, environ) + return sandbox._value_for(sandbox["developer_options"]) + + milestone_path = os.path.join(topsrcdir, "config", "milestone.txt") + with MockedOpen({milestone_path: milestone}): + # developer options are enabled by default on "nightly" milestone + # only + self.assertEqual(get_value(), "a" in milestone or None) + + self.assertEqual(get_value(["--enable-release"]), None) + + self.assertEqual(get_value(environ={"MOZILLA_OFFICIAL": 1}), None) + + self.assertEqual( + get_value(["--enable-release"], environ={"MOZILLA_OFFICIAL": 1}), None + ) + + with self.assertRaises(InvalidOptionError): + get_value(["--disable-release"], environ={"MOZILLA_OFFICIAL": 1}) + + self.assertEqual(get_value(environ={"MOZ_AUTOMATION": 1}), None) + + def test_developer_options_release(self): + self.test_developer_options("42.0") + + +if __name__ == "__main__": + main() diff --git a/python/mozbuild/mozbuild/test/configure/test_util.py b/python/mozbuild/mozbuild/test/configure/test_util.py new file mode 100644 index 0000000000..81c2e2a8bf --- /dev/null +++ b/python/mozbuild/mozbuild/test/configure/test_util.py @@ -0,0 +1,539 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import logging +import os +import sys +import tempfile +import textwrap +import unittest + +import six +from buildconfig import topsrcdir +from mozpack import path as mozpath +from mozunit import main +from six import StringIO + +from common import ConfigureTestSandbox +from mozbuild.configure import ConfigureSandbox +from mozbuild.configure.util import ( + ConfigureOutputHandler, + LineIO, + Version, + getpreferredencoding, +) +from mozbuild.util import exec_ + + +class TestConfigureOutputHandler(unittest.TestCase): + def test_separation(self): + out = StringIO() + err = StringIO() + name = "%s.test_separation" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + logger.addHandler(ConfigureOutputHandler(out, err)) + + logger.error("foo") + logger.warning("bar") + logger.info("baz") + # DEBUG level is not printed out by this handler + logger.debug("qux") + + self.assertEqual(out.getvalue(), "baz\n") + self.assertEqual(err.getvalue(), "foo\nbar\n") + + def test_format(self): + out = StringIO() + err = StringIO() + name = "%s.test_format" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = ConfigureOutputHandler(out, err) + handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) + logger.addHandler(handler) + + logger.error("foo") + logger.warning("bar") + logger.info("baz") + # DEBUG level is not printed out by this handler + logger.debug("qux") + + self.assertEqual(out.getvalue(), "baz\n") + self.assertEqual(err.getvalue(), "ERROR:foo\n" "WARNING:bar\n") + + def test_continuation(self): + out = StringIO() + name = "%s.test_continuation" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = ConfigureOutputHandler(out, out) + handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) + logger.addHandler(handler) + + logger.info("foo") + logger.info("checking bar... ") + logger.info("yes") + logger.info("qux") + + self.assertEqual(out.getvalue(), "foo\n" "checking bar... yes\n" "qux\n") + + out.seek(0) + out.truncate() + + logger.info("foo") + logger.info("checking bar... ") + logger.warning("hoge") + logger.info("no") + logger.info("qux") + + self.assertEqual( + out.getvalue(), + "foo\n" "checking bar... \n" "WARNING:hoge\n" " ... no\n" "qux\n", + ) + + out.seek(0) + out.truncate() + + logger.info("foo") + logger.info("checking bar... ") + logger.warning("hoge") + logger.warning("fuga") + logger.info("no") + logger.info("qux") + + self.assertEqual( + out.getvalue(), + "foo\n" + "checking bar... \n" + "WARNING:hoge\n" + "WARNING:fuga\n" + " ... no\n" + "qux\n", + ) + + out.seek(0) + out.truncate() + err = StringIO() + + logger.removeHandler(handler) + handler = ConfigureOutputHandler(out, err) + handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) + logger.addHandler(handler) + + logger.info("foo") + logger.info("checking bar... ") + logger.warning("hoge") + logger.warning("fuga") + logger.info("no") + logger.info("qux") + + self.assertEqual(out.getvalue(), "foo\n" "checking bar... no\n" "qux\n") + + self.assertEqual(err.getvalue(), "WARNING:hoge\n" "WARNING:fuga\n") + + def test_queue_debug(self): + out = StringIO() + name = "%s.test_queue_debug" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = ConfigureOutputHandler(out, out, maxlen=3) + handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) + logger.addHandler(handler) + + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.info("yes") + logger.info("qux") + + self.assertEqual(out.getvalue(), "checking bar... yes\n" "qux\n") + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.info("no") + logger.error("fail") + + self.assertEqual( + out.getvalue(), "checking bar... no\n" "DEBUG:do foo\n" "ERROR:fail\n" + ) + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.debug("do bar") + logger.debug("do baz") + logger.info("no") + logger.error("fail") + + self.assertEqual( + out.getvalue(), + "checking bar... no\n" + "DEBUG:do foo\n" + "DEBUG:do bar\n" + "DEBUG:do baz\n" + "ERROR:fail\n", + ) + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.debug("do bar") + logger.debug("do baz") + logger.debug("do qux") + logger.debug("do hoge") + logger.info("no") + logger.error("fail") + + self.assertEqual( + out.getvalue(), + "checking bar... no\n" + "DEBUG:<truncated - see config.log for full output>\n" + "DEBUG:do baz\n" + "DEBUG:do qux\n" + "DEBUG:do hoge\n" + "ERROR:fail\n", + ) + + out.seek(0) + out.truncate() + + try: + with handler.queue_debug(): + logger.info("checking bar... ") + logger.debug("do foo") + logger.debug("do bar") + logger.info("no") + e = Exception("fail") + raise e + except Exception as caught: + self.assertIs(caught, e) + + self.assertEqual( + out.getvalue(), "checking bar... no\n" "DEBUG:do foo\n" "DEBUG:do bar\n" + ) + + def test_queue_debug_reentrant(self): + out = StringIO() + name = "%s.test_queue_debug_reentrant" % self.__class__.__name__ + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + handler = ConfigureOutputHandler(out, out, maxlen=10) + handler.setFormatter(logging.Formatter("%(levelname)s| %(message)s")) + logger.addHandler(handler) + + try: + with handler.queue_debug(): + logger.info("outer info") + logger.debug("outer debug") + with handler.queue_debug(): + logger.info("inner info") + logger.debug("inner debug") + e = Exception("inner exception") + raise e + except Exception as caught: + self.assertIs(caught, e) + + self.assertEqual( + out.getvalue(), + "outer info\n" "inner info\n" "DEBUG| outer debug\n" "DEBUG| inner debug\n", + ) + + out.seek(0) + out.truncate() + + try: + with handler.queue_debug(): + logger.info("outer info") + logger.debug("outer debug") + with handler.queue_debug(): + logger.info("inner info") + logger.debug("inner debug") + e = Exception("outer exception") + raise e + except Exception as caught: + self.assertIs(caught, e) + + self.assertEqual( + out.getvalue(), + "outer info\n" "inner info\n" "DEBUG| outer debug\n" "DEBUG| inner debug\n", + ) + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("outer info") + logger.debug("outer debug") + with handler.queue_debug(): + logger.info("inner info") + logger.debug("inner debug") + logger.error("inner error") + self.assertEqual( + out.getvalue(), + "outer info\n" + "inner info\n" + "DEBUG| outer debug\n" + "DEBUG| inner debug\n" + "ERROR| inner error\n", + ) + + out.seek(0) + out.truncate() + + with handler.queue_debug(): + logger.info("outer info") + logger.debug("outer debug") + with handler.queue_debug(): + logger.info("inner info") + logger.debug("inner debug") + logger.error("outer error") + self.assertEqual( + out.getvalue(), + "outer info\n" + "inner info\n" + "DEBUG| outer debug\n" + "DEBUG| inner debug\n" + "ERROR| outer error\n", + ) + + def test_is_same_output(self): + fd1 = sys.stderr.fileno() + fd2 = os.dup(fd1) + try: + self.assertTrue(ConfigureOutputHandler._is_same_output(fd1, fd2)) + finally: + os.close(fd2) + + fd2, path = tempfile.mkstemp() + try: + self.assertFalse(ConfigureOutputHandler._is_same_output(fd1, fd2)) + + fd3 = os.dup(fd2) + try: + self.assertTrue(ConfigureOutputHandler._is_same_output(fd2, fd3)) + finally: + os.close(fd3) + + with open(path, "a") as fh: + fd3 = fh.fileno() + self.assertTrue(ConfigureOutputHandler._is_same_output(fd2, fd3)) + + finally: + os.close(fd2) + os.remove(path) + + +class TestLineIO(unittest.TestCase): + def test_lineio(self): + lines = [] + l = LineIO(lambda l: lines.append(l)) + + l.write("a") + self.assertEqual(lines, []) + + l.write("b") + self.assertEqual(lines, []) + + l.write("\n") + self.assertEqual(lines, ["ab"]) + + l.write("cdef") + self.assertEqual(lines, ["ab"]) + + l.write("\n") + self.assertEqual(lines, ["ab", "cdef"]) + + l.write("ghi\njklm") + self.assertEqual(lines, ["ab", "cdef", "ghi"]) + + l.write("nop\nqrst\nuv\n") + self.assertEqual(lines, ["ab", "cdef", "ghi", "jklmnop", "qrst", "uv"]) + + l.write("wx\nyz") + self.assertEqual(lines, ["ab", "cdef", "ghi", "jklmnop", "qrst", "uv", "wx"]) + + l.close() + self.assertEqual( + lines, ["ab", "cdef", "ghi", "jklmnop", "qrst", "uv", "wx", "yz"] + ) + + def test_lineio_contextmanager(self): + lines = [] + with LineIO(lambda l: lines.append(l)) as l: + l.write("a\nb\nc") + + self.assertEqual(lines, ["a", "b"]) + + self.assertEqual(lines, ["a", "b", "c"]) + + +class TestLogSubprocessOutput(unittest.TestCase): + def test_non_ascii_subprocess_output(self): + out = StringIO() + sandbox = ConfigureSandbox({}, {}, ["configure"], out, out) + + sandbox.include_file( + mozpath.join(topsrcdir, "build", "moz.configure", "util.configure") + ) + sandbox.include_file( + mozpath.join( + topsrcdir, + "python", + "mozbuild", + "mozbuild", + "test", + "configure", + "data", + "subprocess.configure", + ) + ) + status = 0 + try: + sandbox.run() + except SystemExit as e: + status = e.code + + self.assertEqual(status, 0) + quote_char = "'" + if getpreferredencoding().lower() == "utf-8": + quote_char = "\u00B4" + self.assertEqual(six.ensure_text(out.getvalue().strip()), quote_char) + + +class TestVersion(unittest.TestCase): + def test_version_simple(self): + v = Version("1") + self.assertEqual(v, "1") + self.assertLess(v, "2") + self.assertGreater(v, "0.5") + self.assertEqual(v.major, 1) + self.assertEqual(v.minor, 0) + self.assertEqual(v.patch, 0) + + def test_version_more(self): + v = Version("1.2.3b") + self.assertLess(v, "2") + self.assertEqual(v.major, 1) + self.assertEqual(v.minor, 2) + self.assertEqual(v.patch, 3) + + def test_version_bad(self): + # A version with a letter in the middle doesn't really make sense, + # so everything after it should be ignored. + v = Version("1.2b.3") + self.assertLess(v, "2") + self.assertEqual(v.major, 1) + self.assertEqual(v.minor, 2) + self.assertEqual(v.patch, 0) + + def test_version_badder(self): + v = Version("1b.2.3") + self.assertLess(v, "2") + self.assertEqual(v.major, 1) + self.assertEqual(v.minor, 0) + self.assertEqual(v.patch, 0) + + +class TestCheckCmdOutput(unittest.TestCase): + def get_result(self, command="", paths=None): + paths = paths or {} + config = {} + out = StringIO() + sandbox = ConfigureTestSandbox(paths, config, {}, ["/bin/configure"], out, out) + sandbox.include_file( + mozpath.join(topsrcdir, "build", "moz.configure", "util.configure") + ) + status = 0 + try: + exec_(command, sandbox) + sandbox.run() + except SystemExit as e: + status = e.code + return config, out.getvalue(), status + + def test_simple_program(self): + def mock_simple_prog(_, args): + if len(args) == 1 and args[0] == "--help": + return 0, "simple program help...", "" + self.fail("Unexpected arguments to mock_simple_program: %s" % args) + + prog_path = mozpath.abspath("/simple/prog") + cmd = "log.info(check_cmd_output('%s', '--help'))" % prog_path + config, out, status = self.get_result(cmd, paths={prog_path: mock_simple_prog}) + self.assertEqual(config, {}) + self.assertEqual(status, 0) + self.assertEqual(out, "simple program help...\n") + + def test_failing_program(self): + def mock_error_prog(_, args): + if len(args) == 1 and args[0] == "--error": + return (127, "simple program output", "simple program error output") + self.fail("Unexpected arguments to mock_error_program: %s" % args) + + prog_path = mozpath.abspath("/simple/prog") + cmd = "log.info(check_cmd_output('%s', '--error'))" % prog_path + config, out, status = self.get_result(cmd, paths={prog_path: mock_error_prog}) + self.assertEqual(config, {}) + self.assertEqual(status, 1) + self.assertEqual( + out, + textwrap.dedent( + """\ + DEBUG: Executing: `%s --error` + DEBUG: The command returned non-zero exit status 127. + DEBUG: Its output was: + DEBUG: | simple program output + DEBUG: Its error output was: + DEBUG: | simple program error output + ERROR: Command `%s --error` failed with exit status 127. + """ + % (prog_path, prog_path) + ), + ) + + def test_error_callback(self): + def mock_error_prog(_, args): + if len(args) == 1 and args[0] == "--error": + return 127, "simple program error...", "" + self.fail("Unexpected arguments to mock_error_program: %s" % args) + + prog_path = mozpath.abspath("/simple/prog") + cmd = textwrap.dedent( + """\ + check_cmd_output('%s', '--error', + onerror=lambda: die('`prog` produced an error')) + """ + % prog_path + ) + config, out, status = self.get_result(cmd, paths={prog_path: mock_error_prog}) + self.assertEqual(config, {}) + self.assertEqual(status, 1) + self.assertEqual( + out, + textwrap.dedent( + """\ + DEBUG: Executing: `%s --error` + DEBUG: The command returned non-zero exit status 127. + DEBUG: Its output was: + DEBUG: | simple program error... + ERROR: `prog` produced an error + """ + % prog_path + ), + ) + + +if __name__ == "__main__": + main() |