Skip to content

Commit a3fb6ed

Browse files
committed
test: add tests for the installer
1 parent 79f492f commit a3fb6ed

File tree

5 files changed

+683
-1
lines changed

5 files changed

+683
-1
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ repos:
22

33
- repo: https://github.com/charliermarsh/ruff-pre-commit
44
# Ruff version.
5-
rev: 'v0.14.11'
5+
rev: 'v0.14.13'
66
hooks:
77
- id: ruff
88
# Respect `exclude` and `extend-exclude` settings.
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Copyright (c) 2025-2026, Abilian SAS
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""E2E test fixtures for installer testing.
4+
5+
These fixtures provide Docker container management for testing the
6+
hop3-installer scripts in isolation.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import subprocess
12+
import time
13+
from pathlib import Path
14+
from typing import TYPE_CHECKING
15+
16+
import pytest
17+
18+
if TYPE_CHECKING:
19+
from collections.abc import Generator
20+
21+
# Container configuration
22+
CONTAINER_NAME = "hop3-installer-test"
23+
BASE_IMAGE = "ubuntu:24.04"
24+
25+
26+
def docker_exec(
27+
container: str,
28+
cmd: str,
29+
*,
30+
check: bool = True,
31+
user: str | None = None,
32+
) -> subprocess.CompletedProcess:
33+
"""Execute command in Docker container.
34+
35+
Args:
36+
container: Container name or ID
37+
cmd: Command to execute
38+
check: Whether to raise on non-zero exit
39+
user: User to run command as (default: root)
40+
41+
Returns:
42+
CompletedProcess with stdout/stderr captured
43+
"""
44+
docker_cmd = ["docker", "exec"]
45+
if user:
46+
docker_cmd.extend(["-u", user])
47+
docker_cmd.extend([container, "bash", "-c", cmd])
48+
49+
return subprocess.run(
50+
docker_cmd,
51+
capture_output=True,
52+
text=True,
53+
check=check,
54+
)
55+
56+
57+
def docker_copy(container: str, src: Path, dest: str) -> None:
58+
"""Copy file to Docker container.
59+
60+
Args:
61+
container: Container name or ID
62+
src: Local source path
63+
dest: Remote destination path
64+
"""
65+
subprocess.run(
66+
["docker", "cp", str(src), f"{container}:{dest}"],
67+
check=True,
68+
)
69+
70+
71+
def docker_copy_dir(container: str, src: Path, dest: str) -> None:
72+
"""Copy directory to Docker container.
73+
74+
Args:
75+
container: Container name or ID
76+
src: Local source directory
77+
dest: Remote destination path
78+
"""
79+
subprocess.run(
80+
["docker", "cp", str(src), f"{container}:{dest}"],
81+
check=True,
82+
)
83+
# Fix permissions so all users can read
84+
docker_exec(container, f"chmod -R a+rX {dest}", check=False)
85+
86+
87+
@pytest.fixture(scope="module")
88+
def docker_container() -> Generator[str, None, None]:
89+
"""Create and manage a Docker container for testing.
90+
91+
This fixture:
92+
1. Removes any existing test container
93+
2. Starts a fresh Ubuntu container
94+
3. Installs Python and prerequisites
95+
4. Yields the container name
96+
5. Cleans up after tests complete
97+
98+
Yields:
99+
Container name for use in tests
100+
"""
101+
# Check Docker is available
102+
result = subprocess.run(
103+
["docker", "info"],
104+
capture_output=True,
105+
check=False,
106+
)
107+
if result.returncode != 0:
108+
pytest.skip("Docker not available")
109+
110+
# Remove any existing container
111+
subprocess.run(
112+
["docker", "rm", "-f", CONTAINER_NAME],
113+
capture_output=True,
114+
check=False,
115+
)
116+
117+
# Start fresh container
118+
# Note: We use sleep infinity instead of systemd for simplicity
119+
# Service validation tests will be skipped without systemd
120+
result = subprocess.run(
121+
[
122+
"docker",
123+
"run",
124+
"-d",
125+
"--name",
126+
CONTAINER_NAME,
127+
BASE_IMAGE,
128+
"sleep",
129+
"infinity",
130+
],
131+
capture_output=True,
132+
text=True,
133+
check=False,
134+
)
135+
if result.returncode != 0:
136+
pytest.fail(f"Failed to start container: {result.stderr}")
137+
138+
# Wait for container to be ready
139+
time.sleep(1)
140+
141+
# Install prerequisites
142+
docker_exec(
143+
CONTAINER_NAME,
144+
"apt-get update -qq && "
145+
"DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "
146+
"python3 python3-venv python3-pip git curl sudo ca-certificates",
147+
)
148+
149+
yield CONTAINER_NAME
150+
151+
# Cleanup
152+
subprocess.run(
153+
["docker", "rm", "-f", CONTAINER_NAME],
154+
capture_output=True,
155+
check=False,
156+
)
157+
158+
159+
@pytest.fixture(scope="module")
160+
def bundled_installers(tmp_path_factory: pytest.TempPathFactory) -> dict[str, Path]:
161+
"""Generate bundled installer scripts.
162+
163+
This fixture generates the single-file installer scripts using the bundler.
164+
165+
Returns:
166+
Dict with 'cli' and 'server' keys pointing to installer paths
167+
"""
168+
from hop3_installer.bundler import bundle_installer
169+
170+
out_dir = tmp_path_factory.mktemp("installers")
171+
172+
# Generate CLI installer
173+
cli_content = bundle_installer("cli")
174+
cli_path = out_dir / "install-cli.py"
175+
cli_path.write_text(cli_content)
176+
cli_path.chmod(0o755)
177+
178+
# Generate server installer
179+
server_content = bundle_installer("server")
180+
server_path = out_dir / "install-server.py"
181+
server_path.write_text(server_content)
182+
server_path.chmod(0o755)
183+
184+
return {"cli": cli_path, "server": server_path}
185+
186+
187+
@pytest.fixture(scope="module")
188+
def hop3_packages_dir() -> Path:
189+
"""Get the path to the hop3 packages directory.
190+
191+
Returns:
192+
Path to packages/ directory in the repository
193+
"""
194+
# Navigate from this file to the packages directory
195+
# tests/c_e2e/conftest.py -> packages/hop3-installer/tests/c_e2e/conftest.py
196+
this_file = Path(__file__)
197+
packages_dir = this_file.parent.parent.parent.parent.parent / "packages"
198+
199+
if not packages_dir.exists():
200+
pytest.fail(f"Packages directory not found: {packages_dir}")
201+
202+
return packages_dir
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copyright (c) 2025-2026, Abilian SAS
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""E2E tests for CLI installer (install-cli.py).
4+
5+
These tests verify that the bundled CLI installer correctly installs
6+
hop3-cli in various scenarios.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from typing import TYPE_CHECKING
12+
13+
import pytest
14+
15+
from .conftest import docker_copy, docker_copy_dir, docker_exec
16+
17+
if TYPE_CHECKING:
18+
from pathlib import Path
19+
20+
21+
@pytest.mark.e2e
22+
class TestCLIInstaller:
23+
"""Test hop3-cli installation via install-cli.py."""
24+
25+
def test_install_cli_from_git(
26+
self,
27+
docker_container: str,
28+
bundled_installers: dict[str, Path],
29+
) -> None:
30+
"""Test CLI installation from git repository.
31+
32+
This is the primary installation method - installing from the
33+
git repository's devel branch.
34+
"""
35+
# Copy installer to container
36+
docker_copy(docker_container, bundled_installers["cli"], "/tmp/install-cli.py")
37+
38+
# Run installer with git method
39+
result = docker_exec(
40+
docker_container,
41+
"python3 /tmp/install-cli.py --git --branch devel --no-modify-path --verbose",
42+
check=False,
43+
)
44+
45+
# Check installer succeeded
46+
assert result.returncode == 0, (
47+
f"CLI installer failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
48+
)
49+
50+
# Validate installation
51+
self._validate_cli_installation(docker_container)
52+
53+
def test_install_cli_from_local_path(
54+
self,
55+
docker_container: str,
56+
bundled_installers: dict[str, Path],
57+
hop3_packages_dir: Path,
58+
) -> None:
59+
"""Test CLI installation from local package path.
60+
61+
This tests the --local-path option used during development.
62+
"""
63+
# Copy installer to container
64+
docker_copy(docker_container, bundled_installers["cli"], "/tmp/install-cli.py")
65+
66+
# Copy the hop3-cli package to container
67+
cli_package = hop3_packages_dir / "hop3-cli"
68+
if not cli_package.exists():
69+
pytest.skip(f"hop3-cli package not found: {cli_package}")
70+
71+
docker_copy_dir(docker_container, cli_package, "/tmp/hop3-cli")
72+
73+
# Clean up any previous installation
74+
docker_exec(
75+
docker_container,
76+
"rm -rf ~/.hop3-cli",
77+
check=False,
78+
)
79+
80+
# Run installer with local path
81+
result = docker_exec(
82+
docker_container,
83+
"python3 /tmp/install-cli.py --local-path /tmp/hop3-cli --no-modify-path --verbose",
84+
check=False,
85+
)
86+
87+
# Check installer succeeded
88+
assert result.returncode == 0, (
89+
f"CLI installer failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"
90+
)
91+
92+
# Validate installation
93+
self._validate_cli_installation(docker_container)
94+
95+
def _validate_cli_installation(self, container: str) -> None:
96+
"""Validate CLI was installed correctly.
97+
98+
Checks:
99+
1. Virtual environment was created
100+
2. hop3 command exists in venv
101+
3. hop3 command runs without crashing
102+
"""
103+
# Check venv exists
104+
result = docker_exec(container, "test -d ~/.hop3-cli/venv", check=False)
105+
assert result.returncode == 0, "CLI virtual environment not created"
106+
107+
# Check hop3 command exists
108+
result = docker_exec(
109+
container,
110+
"test -f ~/.hop3-cli/venv/bin/hop3 || test -f ~/.hop3-cli/venv/bin/hop",
111+
check=False,
112+
)
113+
assert result.returncode == 0, "hop3 command not found in venv"
114+
115+
# Check hop3 runs (version command doesn't need server config)
116+
result = docker_exec(
117+
container,
118+
"~/.hop3-cli/venv/bin/hop3 version 2>&1 || true",
119+
check=False,
120+
)
121+
# The command might fail if no server is configured, but it should at least
122+
# print something about hop3
123+
output = (result.stdout + result.stderr).lower()
124+
assert "hop3" in output or result.returncode == 0, (
125+
f"hop3 command doesn't run properly: {result.stdout} {result.stderr}"
126+
)

0 commit comments

Comments
 (0)