Source code

Revision control

Copy as Markdown

Other Tools

# 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
from functools import cached_property
from io import StringIO
from buildconfig import topobjdir, topsrcdir
from mozfile import json
from mozpack import path as mozpath
from mozbuild.configure import ConfigureSandbox
from mozbuild.util import ReadOnlyNamespace
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:
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 paths.items() 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)
self._dependency_overrides = {}
super().__init__(config, environ, *args, **kwargs)
def _value_for_depends(self, obj):
if obj in self._dependency_overrides:
return self._dependency_overrides[obj]
return super()._value_for_depends(obj)
@cached_property
def _wrapped_mozfile(self):
return ReadOnlyNamespace(which=self.which, json=json)
@cached_property
def _wrapped_os(self):
return self.imported_os
@cached_property
def _wrapped_subprocess(self):
return ReadOnlyNamespace(
CalledProcessError=subprocess.CalledProcessError,
check_output=self.check_output,
run=self.subprocess_run,
PIPE=subprocess.PIPE,
STDOUT=subprocess.STDOUT,
Popen=self.Popen,
)
@cached_property
def _wrapped_ctypes(self):
class CTypesFunc:
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),
)
@cached_property
def _wrapped__winreg(self):
def OpenKey(*args, **kwargs):
raise OSError()
return ReadOnlyNamespace(HKEY_LOCAL_MACHINE=0, OpenKey=OpenKey)
def create_unicode_buffer(self, *args, **kwargs):
class Buffer:
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, str):
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)
cwd = kargs.get("cwd")
if cwd and func.__code__.co_argcount == 3:
retcode, stdout, stderr = func(stdin, args[1:], cwd)
else:
retcode, stdout, stderr = func(stdin, args[1:])
class Process:
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 subprocess_run(self, args, **kwargs):
proc = self.Popen(args, **kwargs)
stdout, stderr = proc.communicate()
retcode = proc.wait()
if kwargs.get("check") and retcode:
raise subprocess.CalledProcessError(retcode, args, stdout)
return ReadOnlyNamespace(
args=args,
returncode=retcode,
stdout=stdout,
stderr=stderr,
)
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, mozconfig.encode())
os.close(fh)
else:
mozconfig_path = os.path.join(
os.path.dirname(__file__), "data", "empty_mozconfig"
)
try:
environ = dict(
environ,
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)