107 lines
5.3 KiB
Python
107 lines
5.3 KiB
Python
"""Post-PM installer stages use the real installation-bound publication."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
from tests.installation_launcher_fixture import publish_fixture_launcher
|
|
|
|
if os.name == 'posix':
|
|
import fcntl
|
|
import pty
|
|
import termios
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
def _with_controlling_terminal(tty_path: str):
|
|
"""preexec_fn: make this PTY the child's controlling terminal, so /dev/tty opens."""
|
|
def attach() -> None:
|
|
os.setsid() # windows-footgun: ok (posix-only test)
|
|
fd = os.open(tty_path, os.O_RDWR)
|
|
try:
|
|
# Opening a PTY slave does not always acquire it on Darwin.
|
|
fcntl.ioctl(fd, termios.TIOCSCTTY, 0)
|
|
probe = os.open('/dev/tty', os.O_RDONLY)
|
|
os.close(probe)
|
|
finally:
|
|
os.close(fd)
|
|
return attach
|
|
|
|
|
|
@pytest.mark.platforms('posix')
|
|
@pytest.mark.parametrize('stage, expected', [('setup', ['setup']), ('gateway', ['gateway', 'install', '--if-missing'])])
|
|
def test_installer_post_pm_stages(tmp_path: Path, stage: str, expected: list[str]) -> None:
|
|
install = tmp_path / 'source tree'
|
|
calls = tmp_path / 'calls.json'
|
|
publish_fixture_launcher(install, "import json, os, sys\nfrom pathlib import Path\ndef main():\n Path(os.environ['CALLS']).write_text(json.dumps(sys.argv[1:])); return int(os.environ['STAGE_EXIT'])\n")
|
|
command = ['bash', '-c', 'source "$1" --manifest >/dev/null; INSTALL_DIR="$2"; NON_INTERACTIVE=false; "stage_$3"', 'test', str(ROOT / 'scripts/install.sh'), str(install), stage]
|
|
env = {**os.environ, 'HOME': str(tmp_path), 'HERMES_HOME': str(tmp_path / 'home'), 'HERMES_RUNTIME_DIR': str(tmp_path / 'store'), 'CALLS': str(calls)}
|
|
# `curl | bash` and Docker builds have no terminal: the interactive stage is skipped, not failed.
|
|
result = subprocess.run(command, cwd=tmp_path, env={**env, 'STAGE_EXIT': '9'}, capture_output=True, text=True,
|
|
encoding='utf-8', timeout=20, start_new_session=True, check=False)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert not calls.exists()
|
|
master, slave = pty.openpty()
|
|
try:
|
|
tty_path = os.ttyname(slave)
|
|
finally:
|
|
os.close(slave)
|
|
try:
|
|
for code in [0, 9]:
|
|
result = subprocess.run(command, cwd=tmp_path, env={**env, 'STAGE_EXIT': str(code)}, capture_output=True,
|
|
text=True, encoding='utf-8', timeout=20, stdin=subprocess.DEVNULL,
|
|
preexec_fn=_with_controlling_terminal(tty_path), check=False)
|
|
assert calls.exists(), (result.returncode, result.stdout, result.stderr)
|
|
assert (result.returncode == 0) == (code == 0), result.stdout + result.stderr
|
|
assert json.loads(calls.read_text()) == expected
|
|
finally:
|
|
os.close(master)
|
|
assert not (install / 'venv').exists()
|
|
|
|
|
|
@pytest.mark.platforms('posix')
|
|
@pytest.mark.parametrize('stage, include_desktop, expected_desktop', [
|
|
('products', False, False), ('products', True, True), ('desktop', False, True),
|
|
])
|
|
def test_products_and_desktop_stages_share_the_completion_tail(tmp_path: Path, stage: str,
|
|
include_desktop: bool, expected_desktop: bool) -> None:
|
|
"""Both stages hand the checkout to hermes_cli/source_completion.py; --include-desktop
|
|
(or the external `desktop` stage) only adds --desktop to that one call."""
|
|
install = tmp_path / 'source tree'
|
|
calls = tmp_path / 'calls.json'
|
|
(install / 'hermes_cli').mkdir(parents=True)
|
|
(install / 'hermes_cli' / 'source_completion.py').write_text(
|
|
"import json, os, sys\nfrom pathlib import Path\n"
|
|
"Path(os.environ['CALLS']).write_text(json.dumps(sys.argv[1:]))\n", encoding='utf-8')
|
|
flag = 'true' if include_desktop else 'false'
|
|
command = ['bash', '-c',
|
|
f'source "$1" --manifest >/dev/null; INSTALL_DIR="$2"; INCLUDE_DESKTOP={flag}; '
|
|
'bootstrap_python() { boot_py="$PYTHON_FOR_TEST"; }; "stage_$3"',
|
|
'test', str(ROOT / 'scripts/install.sh'), str(install), stage]
|
|
env = {**os.environ, 'HOME': str(tmp_path), 'HERMES_HOME': str(tmp_path / 'home'),
|
|
'HERMES_RUNTIME_DIR': str(tmp_path / 'store'), 'CALLS': str(calls), 'PYTHON_FOR_TEST': sys.executable}
|
|
result = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=20, check=False)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
argv = json.loads(calls.read_text())
|
|
assert argv[:2] == ['--source', str(install)]
|
|
assert ('--desktop' in argv) is expected_desktop
|
|
|
|
|
|
@pytest.mark.platforms("posix")
|
|
def test_desktop_flag_does_not_add_a_stage(tmp_path):
|
|
"""--include-desktop selects the desktop product inside `products`; the manifest is one ladder."""
|
|
def manifest(*flags):
|
|
result = subprocess.run(
|
|
["bash", str(ROOT / "scripts/install.sh"), "--manifest", *flags],
|
|
cwd=tmp_path, capture_output=True, text=True, timeout=30, check=True,
|
|
)
|
|
return [row["name"] for row in json.loads(result.stdout)["stages"]]
|
|
|
|
plain = manifest()
|
|
assert "desktop" not in plain
|
|
assert "products" in plain
|
|
assert plain[-1] == "complete"
|
|
assert manifest("--include-desktop") == plain
|