The setup stage installs the gateway service through ensure_gateway_service. On Windows that asks the start-now, Scheduled Task and UAC questions. The gateway stage then ran `hermes gateway install`, which asked them all again. `gateway install --if-missing` does nothing when a service is already installed. Both installers' gateway stages use it, so they ask only when setup did not install the service.
107 lines
5.2 KiB
Python
107 lines
5.2 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)
|
|
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))
|
|
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)
|
|
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
|