Skip to content

Commit 6c11442

Browse files
authored
test(smoke): use shared app test helpers
Replace the copied app test helper body with the shared aio_fleet.app_testing shim while keeping repo-local tests in place.
1 parent 22b3b8a commit 6c11442

1 file changed

Lines changed: 4 additions & 249 deletions

File tree

tests/helpers.py

Lines changed: 4 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -1,254 +1,9 @@
11
from __future__ import annotations
22

3-
import os
4-
import shlex
5-
import shutil
6-
import socket
7-
import subprocess # nosec B404 - test helpers shell out only to local tooling
8-
import time
9-
import uuid
10-
from collections.abc import Iterator
11-
from contextlib import contextmanager
123
from pathlib import Path
134

14-
REPO_ROOT = Path(__file__).resolve().parents[1]
15-
16-
17-
def run_command(
18-
command: list[str],
19-
*,
20-
cwd: Path | None = None,
21-
env: dict[str, str] | None = None,
22-
check: bool = True,
23-
capture_output: bool = True,
24-
) -> subprocess.CompletedProcess[str]:
25-
return subprocess.run( # nosec B603 - tests execute trusted local commands only
26-
command,
27-
cwd=cwd or REPO_ROOT,
28-
env=env,
29-
check=check,
30-
text=True,
31-
capture_output=capture_output,
32-
)
33-
34-
35-
def docker_available() -> bool:
36-
if shutil.which("docker") is None:
37-
return False
38-
39-
result = run_command(["docker", "info"], check=False)
40-
return result.returncode == 0
41-
42-
43-
def docker_image_exists(image_tag: str) -> bool:
44-
result = run_command(["docker", "image", "inspect", image_tag], check=False)
45-
return result.returncode == 0
46-
47-
48-
def ensure_pytest_image(
49-
image_tag: str,
50-
*,
51-
dockerfile: str = "Dockerfile",
52-
prebuilt_env: str = "AIO_PYTEST_USE_PREBUILT_IMAGE",
53-
) -> None:
54-
if os.environ.get(prebuilt_env) == "true":
55-
if not docker_image_exists(image_tag):
56-
raise AssertionError(
57-
f"Expected prebuilt pytest image {image_tag} to be loaded before the test run."
58-
)
59-
return
60-
61-
command = ["docker", "build", "--platform", "linux/amd64", "-t", image_tag]
62-
if dockerfile != "Dockerfile":
63-
command.extend(["-f", dockerfile])
64-
command.append(".")
65-
run_command(command)
66-
67-
68-
def reserve_host_port() -> int:
69-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
70-
sock.bind(("127.0.0.1", 0))
71-
sock.listen(1)
72-
return sock.getsockname()[1]
73-
74-
75-
def create_docker_volume(prefix: str) -> str:
76-
volume_name = f"{prefix}-{uuid.uuid4().hex[:10]}"
77-
run_command(["docker", "volume", "create", volume_name])
78-
return volume_name
79-
80-
81-
def remove_docker_volume(volume_name: str) -> None:
82-
run_command(["docker", "volume", "rm", "-f", volume_name], check=False)
83-
84-
85-
@contextmanager
86-
def docker_volume(prefix: str) -> Iterator[str]:
87-
volume_name = create_docker_volume(prefix)
88-
try:
89-
yield volume_name
90-
finally:
91-
remove_docker_volume(volume_name)
92-
93-
94-
def docker_exec(
95-
container_name: str, command: str, *, check: bool = True
96-
) -> subprocess.CompletedProcess[str]:
97-
return run_command(
98-
["docker", "exec", container_name, "sh", "-lc", command], check=check
99-
)
100-
101-
102-
def container_path_exists(container_name: str, path: str) -> bool:
103-
return (
104-
docker_exec(
105-
container_name, f"test -e {shlex.quote(path)}", check=False
106-
).returncode
107-
== 0
108-
)
109-
110-
111-
def read_container_file(container_name: str, path: str) -> str:
112-
return docker_exec(container_name, f"cat {shlex.quote(path)}").stdout
113-
5+
from aio_fleet.app_testing import * # noqa: F403
6+
from aio_fleet.app_testing import configure_repo_root
1147

115-
def container_file_size(container_name: str, path: str) -> int:
116-
return int(
117-
docker_exec(container_name, f"wc -c < {shlex.quote(path)}").stdout.strip()
118-
)
119-
120-
121-
class DockerRuntime:
122-
def __init__(self, image_tag: str) -> None:
123-
self.image_tag = image_tag
124-
125-
def build(self) -> None:
126-
ensure_pytest_image(self.image_tag)
127-
128-
def inspect_state(self, name: str, field: str) -> str:
129-
result = run_command(
130-
["docker", "inspect", "-f", f"{{{{.{field}}}}}", name],
131-
check=False,
132-
)
133-
return result.stdout.strip() if result.returncode == 0 else ""
134-
135-
def logs(self, name: str) -> str:
136-
result = run_command(["docker", "logs", name], check=False)
137-
return result.stdout + result.stderr
138-
139-
def remove(self, name: str) -> None:
140-
run_command(["docker", "rm", "-f", name], check=False)
141-
142-
@contextmanager
143-
def container(
144-
self,
145-
*,
146-
env_overrides: dict[str, str] | None = None,
147-
) -> Iterator["ContainerHandle"]:
148-
suffix = uuid.uuid4().hex[:10]
149-
name = f"aio-template-pytest-{suffix}"
150-
http_port = reserve_host_port()
151-
config_volume = create_docker_volume(f"{name}-config")
152-
data_volume = create_docker_volume(f"{name}-data")
153-
try:
154-
command = [
155-
"docker",
156-
"run",
157-
"-d",
158-
"--platform",
159-
"linux/amd64",
160-
"--name",
161-
name,
162-
"-p",
163-
f"{http_port}:8080",
164-
"-v",
165-
f"{config_volume}:/config",
166-
"-v",
167-
f"{data_volume}:/data",
168-
]
169-
170-
if env_overrides:
171-
for key, value in env_overrides.items():
172-
command.extend(["-e", f"{key}={value}"])
173-
174-
command.append(self.image_tag)
175-
run_command(command)
176-
handle = ContainerHandle(
177-
runtime=self,
178-
name=name,
179-
http_port=http_port,
180-
config_volume=config_volume,
181-
data_volume=data_volume,
182-
)
183-
try:
184-
yield handle
185-
finally:
186-
self.remove(name)
187-
finally:
188-
remove_docker_volume(config_volume)
189-
remove_docker_volume(data_volume)
190-
191-
192-
class ContainerHandle:
193-
def __init__(
194-
self,
195-
*,
196-
runtime: DockerRuntime,
197-
name: str,
198-
http_port: int,
199-
config_volume: str,
200-
data_volume: str,
201-
) -> None:
202-
self.runtime = runtime
203-
self.name = name
204-
self.http_port = http_port
205-
self.config_volume = config_volume
206-
self.data_volume = data_volume
207-
208-
def logs(self) -> str:
209-
return self.runtime.logs(self.name)
210-
211-
def exec(
212-
self, command: str, *, check: bool = True
213-
) -> subprocess.CompletedProcess[str]:
214-
return docker_exec(self.name, command, check=check)
215-
216-
def restart(self) -> None:
217-
run_command(["docker", "restart", self.name])
218-
219-
def is_running(self) -> bool:
220-
return self.runtime.inspect_state(self.name, "State.Status") == "running"
221-
222-
def path_exists(self, path: str) -> bool:
223-
return container_path_exists(self.name, path)
224-
225-
def read_text(self, path: str) -> str:
226-
return read_container_file(self.name, path)
227-
228-
def file_size(self, path: str) -> int:
229-
return container_file_size(self.name, path)
230-
231-
def wait_for_http(self, *, path: str = "/health", timeout: int = 180) -> None:
232-
deadline = time.time() + timeout
233-
url = f"http://127.0.0.1:{self.http_port}{path}"
234-
235-
while time.time() < deadline:
236-
if not self.is_running():
237-
raise AssertionError(
238-
f"{self.name} stopped before HTTP became healthy.\nLogs:\n{self.logs()}"
239-
)
240-
241-
result = run_command(["curl", "-fsS", url], check=False)
242-
if result.returncode == 0:
243-
return
244-
time.sleep(2)
245-
246-
raise AssertionError(
247-
f"{self.name} did not become healthy.\nLogs:\n{self.logs()}"
248-
)
249-
250-
251-
def pytest_env(base_env: dict[str, str] | None = None) -> dict[str, str]:
252-
env = dict(os.environ if base_env is None else base_env)
253-
env.setdefault("PYTHONUNBUFFERED", "1")
254-
return env
8+
REPO_ROOT = Path(__file__).resolve().parents[1]
9+
configure_repo_root(REPO_ROOT)

0 commit comments

Comments
 (0)