fix: tools/bugreport launcher finds a Python 3.8+ itself and never starts a Store alias (#166)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2 Files changed: - .gitea/workflows/ci.yml - CHANGES.md - INSTALL.md - VERSION - instructions/bug-report.md - tools/README.md - tools/bugreport - tools/bugreport.ps1 - tools/bugreport.py - tools/chemenu/tests/test_bugreport_launcher.py - tools/chemenu/tests/test_instructions_shell.py - tools/chemenu/tests/test_portability.py
This commit is contained in:
1 parent
c77bda2004
commit
5d937233b1
12 files changed
+539
-21
No files matched your search
@@ -0,0 +1,184 @@
|
||||
"""`tools/bugreport` and `tools/bugreport.ps1`, the collector's launchers (Gitea #166).
|
||||
|
||||
The collector has to run when nothing else does, and on Windows `python3` - in PowerShell
|
||||
also `python` - can be the Microsoft Store's alias. So the launchers find a Python 3.8+
|
||||
themselves, never start anything under WindowsApps, and assume nothing the preflight sets
|
||||
up. These tests drive both against the stub machine from `test_preflight.py`: a `PATH` the
|
||||
test builds, stub Pythons in it, and no `.wikitool-tools.json` anywhere.
|
||||
|
||||
A stub Python answers the launcher's probe (`-c ...`) with 0, or 3 for "too old", and
|
||||
otherwise prints the arguments it was started with - which is how a test sees that the
|
||||
collector was run, and with what.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from chemenu.tests.test_preflight import SHELLS, TOOLS, Machine
|
||||
|
||||
PWSH = shutil.which("pwsh")
|
||||
|
||||
# Answers the probe as a current Python, otherwise echoes its arguments.
|
||||
PYTHON = "#!/bin/sh\n[ \"$1\" = -c ] && exit 0\nprintf '%s\\n' \"$@\"\n"
|
||||
# The same for the `py` launcher, which needs -3 in front of the probe.
|
||||
PY_LAUNCHER = "#!/bin/sh\n[ \"$1\" = -3 ] && [ \"$2\" = -c ] && exit 0\nprintf '%s\\n' \"$@\"\n"
|
||||
TOO_OLD = "#!/bin/sh\n[ \"$1\" = -c ] && exit 3\necho 'collector run by a Python that is too old'\n"
|
||||
REFUSED = "#!/bin/sh\necho 'Access is denied.' >&2\nexit 1\n"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def machine(tmp_path: Path) -> Machine:
|
||||
machine = Machine(tmp_path.resolve())
|
||||
for name in ("bugreport", "bugreport.ps1", "bugreport.py"):
|
||||
shutil.copy2(TOOLS / name, machine.tools / name)
|
||||
return machine
|
||||
|
||||
|
||||
def _env(machine: Machine) -> dict[str, str]:
|
||||
return {
|
||||
"PATH": os.pathsep.join(str(d) for d in machine.path_dirs),
|
||||
"HOME": str(machine.base),
|
||||
**machine.extra_env,
|
||||
}
|
||||
|
||||
|
||||
def _sh(machine: Machine, *args: str, shell: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run([shell, str(machine.tools / "bugreport"), *args],
|
||||
capture_output=True, text=True, env=_env(machine), timeout=30)
|
||||
|
||||
|
||||
def _pwsh(machine: Machine, *args: str) -> subprocess.CompletedProcess:
|
||||
env = {**_env(machine), "DOTNET_CLI_TELEMETRY_OPTOUT": "1", "POWERSHELL_TELEMETRY_OPTOUT": "1"}
|
||||
return subprocess.run(
|
||||
[PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(machine.tools / "bugreport.ps1"), *args],
|
||||
capture_output=True, text=True, env=env, timeout=120,
|
||||
)
|
||||
|
||||
|
||||
def _runners():
|
||||
runners = [pytest.param(lambda m, *a, shell=shell: _sh(m, *a, shell=shell), id=Path(shell).name)
|
||||
for shell in SHELLS]
|
||||
runners.append(pytest.param(
|
||||
_pwsh, id="pwsh", marks=pytest.mark.skipif(PWSH is None, reason="PowerShell 7 (pwsh) is not installed"),
|
||||
))
|
||||
return runners
|
||||
|
||||
|
||||
@pytest.fixture(params=_runners())
|
||||
def launch(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def _store_alias(machine: Machine, name: str) -> Path:
|
||||
"""A Store alias first on PATH that leaves a marker if anything ever starts it."""
|
||||
marker = machine.base / f"{name}-alias-was-run"
|
||||
apps = machine.base / "Users" / "u" / "AppData" / "Local" / "Microsoft" / "WindowsApps"
|
||||
machine.stub(name, f"#!/bin/sh\necho ran > '{marker}'\nexit 9009\n", apps)
|
||||
machine.path_dirs.insert(0, apps)
|
||||
return marker
|
||||
|
||||
|
||||
def test_runs_the_collector_with_every_argument(machine, launch):
|
||||
machine.stub("python3", PYTHON)
|
||||
result = launch(machine, "--chronology", "reports/a b.md", "--no-trace")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines() == [
|
||||
str(machine.tools / "bugreport.py"), "--chronology", "reports/a b.md", "--no-trace",
|
||||
]
|
||||
|
||||
|
||||
def test_needs_nothing_the_preflight_sets_up(machine, launch):
|
||||
machine.stub("python3", PYTHON)
|
||||
assert launch(machine, "--no-trace").returncode == 0
|
||||
assert not (machine.root / ".wikitool-tools.json").exists()
|
||||
assert not (machine.tools / ".venv").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform, alias", [("linux", "python3"), ("windows", "python")])
|
||||
def test_a_store_alias_is_never_started(machine, launch, platform, alias):
|
||||
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = platform
|
||||
marker = _store_alias(machine, alias)
|
||||
machine.stub(alias, PYTHON)
|
||||
result = launch(machine, "--no-trace")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
|
||||
assert not marker.exists()
|
||||
|
||||
|
||||
def test_on_windows_python_comes_before_python3(machine, launch):
|
||||
"""In Git Bash on the Windows target, `python` is the real one and `python3` the alias
|
||||
(T4) - the order alone must not depend on the alias being recognised."""
|
||||
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = "windows"
|
||||
machine.stub("python3", "#!/bin/sh\necho 'wrong python'\n")
|
||||
machine.stub("python", PYTHON)
|
||||
result = launch(machine, "--no-trace")
|
||||
assert result.stdout.splitlines() == [str(machine.tools / "bugreport.py"), "--no-trace"]
|
||||
|
||||
|
||||
def test_on_windows_the_py_launcher_gets_minus_3(machine, launch):
|
||||
machine.extra_env["CHEMENU_PREFLIGHT_PLATFORM"] = "windows"
|
||||
machine.stub("py", PY_LAUNCHER)
|
||||
result = launch(machine, "--no-trace")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines() == ["-3", str(machine.tools / "bugreport.py"), "--no-trace"]
|
||||
|
||||
|
||||
def test_a_python_too_old_is_passed_over(machine, launch):
|
||||
old = machine.stub("python3", TOO_OLD)
|
||||
machine.stub("python", PYTHON)
|
||||
result = launch(machine, "--no-trace")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "too old" not in result.stdout
|
||||
assert str(old) not in result.stdout
|
||||
|
||||
|
||||
def test_a_python_that_refuses_to_start_is_passed_over(machine, launch):
|
||||
machine.stub("python3", REFUSED)
|
||||
machine.stub("python", PYTHON)
|
||||
result = launch(machine, "--no-trace")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", [("bin", "python"), ("Scripts", "python.exe")])
|
||||
def test_the_venv_python_is_the_last_resort(machine, launch, layout):
|
||||
machine.stub(layout[1], PYTHON, machine.tools / ".venv" / layout[0])
|
||||
result = launch(machine, "--no-trace")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines()[0] == str(machine.tools / "bugreport.py")
|
||||
|
||||
|
||||
def test_without_a_python_it_says_why_and_exits_1(machine, launch):
|
||||
old = machine.stub("python3", TOO_OLD)
|
||||
marker = _store_alias(machine, "python")
|
||||
result = launch(machine, "--no-trace")
|
||||
assert result.returncode == 1
|
||||
assert "No Python 3.8 or newer" in result.stderr
|
||||
assert "WindowsApps" in result.stderr
|
||||
assert str(old) in result.stderr
|
||||
assert "<full path to python> tools/bugreport.py" in result.stderr
|
||||
assert not marker.exists()
|
||||
|
||||
|
||||
def test_the_collectors_exit_code_passes_through(machine, launch):
|
||||
machine.stub("python3", "#!/bin/sh\n[ \"$1\" = -c ] && exit 0\nexit 1\n")
|
||||
assert launch(machine, "--bundle", "b", "--candidates", "c").returncode == 1
|
||||
|
||||
|
||||
def test_both_launchers_ship_and_the_sh_one_is_executable():
|
||||
assert (TOOLS / "bugreport").is_file() and (TOOLS / "bugreport.ps1").is_file()
|
||||
assert os.access(TOOLS / "bugreport", os.X_OK)
|
||||
|
||||
|
||||
def test_the_real_collector_runs_through_the_launcher(tmp_path):
|
||||
"""End to end on this machine's own Python: the launcher finds it and the collector's
|
||||
usage comes back, naming the launcher as the program."""
|
||||
result = subprocess.run([str(TOOLS / "bugreport"), "--help"], capture_output=True, text=True,
|
||||
timeout=60, cwd=tmp_path)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.startswith("usage: tools/bugreport")
|
||||
@@ -35,6 +35,10 @@ FORBIDDEN = (
|
||||
("sha256sum", re.compile(r"\bsha256sum\b")),
|
||||
("curl", re.compile(r"\bcurl\b")),
|
||||
("tar", re.compile(r"(^|[;&|]\s*)tar\s")),
|
||||
# Which name starts a real Python differs per shell and platform: on Windows `python3` - in
|
||||
# PowerShell also `python` - can be the Microsoft Store's alias. A command goes through a
|
||||
# launcher that finds the interpreter itself (`tools/wikitool`, `tools/bugreport`).
|
||||
("bare interpreter", re.compile(r"(^|[;&|]\s*)(python3?|py)(\.exe)?\s")),
|
||||
)
|
||||
|
||||
# The two sanctioned exceptions, each one line per shell (instructions/CONTRACT.md): the session
|
||||
|
||||
@@ -396,6 +396,7 @@ def test_output_into_a_pipe_is_utf8_even_under_a_cp1252_locale():
|
||||
@pytest.mark.parametrize("path, eol", [
|
||||
("tools/wikitool", "lf"),
|
||||
("tools/trace-hook", "lf"),
|
||||
("tools/bugreport", "lf"),
|
||||
("tools/preflight.sh", "lf"),
|
||||
("AGENTS.md", "lf"),
|
||||
])
|
||||
|
||||
Reference in new issue
Block a user