Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

# 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/.
"""Regression tests for Bug 2019789.
The newtab mach subcommands spawn a child mach process. Passing a bare,
relative, extension-less "./mach" as argv[0] crashes on Windows with
WinError 193 (CreateProcess cannot run a shebang script as a Win32 process)
and assumes CWD == topsrcdir. The fix launches via the running interpreter
(sys.executable) with an absolute path to mach. These tests pin that argv
shape so a relative "./mach" cannot creep back in.
"""
import importlib.util
import os
import sys
from os import path
from unittest import mock
import mozunit
from mach.registrar import Registrar
NEWTAB_DIR = path.abspath(path.join(path.dirname(__file__), path.pardir, path.pardir))
# Third-party and generated modules that mach_commands imports at module level.
# We only need them to satisfy the import; none are used by the code paths under
# test. mach.decorators is deliberately NOT stubbed: the @Command/@SubCommand
# decorators must run for real so the command functions register and remain
# callable.
_STUBBED_IMPORTS = (
"requests",
"yaml",
"colorama",
"taskcluster",
"glean_utils",
"run_glean_parser",
)
def _load_mach_commands():
# Importing mach_commands runs the @Command/@SubCommand decorators, which
# register into the global mach Registrar. In a bare pytest process no
# categories are registered, so pre-register the "misc" category that the
# newtab command declares; otherwise registration raises MachError.
if "misc" not in Registrar.categories:
Registrar.register_category("misc", "Misc", "Miscellaneous commands.")
# Install the stubs only for the duration of the import, then restore
# sys.modules. Some of these (yaml, colorama) are real modules that pytest
# plugins import later; leaving fakes in sys.modules would break plugin
# bootstrap during pytest.main().
saved = {name: sys.modules.get(name) for name in _STUBBED_IMPORTS}
for name in _STUBBED_IMPORTS:
sys.modules[name] = mock.MagicMock()
try:
spec = importlib.util.spec_from_file_location(
"newtab_mach_commands", path.join(NEWTAB_DIR, "mach_commands.py")
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
finally:
for name, original in saved.items():
if original is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = original
return module
mach_commands = _load_mach_commands()
def _command_context():
ctx = mock.MagicMock()
ctx.topsrcdir = os.path.join("/fake", "topsrcdir")
return ctx
def _assert_portable_mach_argv(argv):
# The exact defect from Bug 2019789: a bare relative shebang path.
assert argv[0] != "./mach"
# Launch via the running interpreter, not by relying on the OS honoring
# the mach shebang.
assert argv[0] == sys.executable
# Absolute mach path, so the spawn does not assume CWD == topsrcdir.
assert os.path.isabs(argv[1])
assert os.path.basename(argv[1]) == "mach"
def test_mach_argv_uses_interpreter_and_absolute_path():
ctx = _command_context()
assert mach_commands.mach_argv(ctx) == [
sys.executable,
os.path.join(ctx.topsrcdir, "mach"),
]
def test_install_spawns_via_interpreter_not_relative_mach():
ctx = _command_context()
with mock.patch.object(mach_commands.subprocess, "Popen") as popen:
popen.return_value.returncode = 0
popen.return_value.wait.return_value = 0
mach_commands.install(ctx)
popen.assert_called_once()
_assert_portable_mach_argv(popen.call_args.args[0])
def test_bundle_spawns_via_interpreter_not_relative_mach():
ctx = _command_context()
with mock.patch.object(mach_commands.subprocess, "Popen") as popen:
popen.return_value.returncode = 0
popen.return_value.wait.return_value = 0
mach_commands.bundle(ctx)
popen.assert_called_once()
_assert_portable_mach_argv(popen.call_args.args[0])
def test_watch_spawns_both_children_via_interpreter():
ctx = _command_context()
with mock.patch.object(
mach_commands.subprocess, "Popen"
) as popen, mock.patch.object(
mach_commands.time, "sleep", side_effect=KeyboardInterrupt
), mock.patch.object(mach_commands, "run_mach"):
mach_commands.watch(ctx)
assert popen.call_count == 2
for call in popen.call_args_list:
_assert_portable_mach_argv(call.args[0])
if __name__ == "__main__":
mozunit.main()