Skip to content

Commit 8c9b178

Browse files
committed
Add integration tests for cli
1 parent 419faa3 commit 8c9b178

4 files changed

Lines changed: 340 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Runtime Integration
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
jobs:
9+
version-runtime:
10+
runs-on: ubuntu-latest
11+
timeout-minutes: 20
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
scenario:
16+
- docker-only
17+
- podman-only
18+
- none
19+
- both-auto
20+
- both-default-docker
21+
- both-default-podman
22+
- both-switch
23+
24+
env:
25+
VP_REAL_RUNTIME_SCENARIO: ${{ matrix.scenario }}
26+
VP_RUNTIME_PROBE_TIMEOUT: "20"
27+
PYTHONUNBUFFERED: "1"
28+
29+
steps:
30+
- name: Check out repository
31+
uses: actions/checkout@v4
32+
33+
- name: Set up Python
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: "3.12"
37+
38+
- name: Install dependencies
39+
run: |
40+
python -m pip install --upgrade pip
41+
python -m pip install -e ".[dev]"
42+
43+
- name: Prepare runtime scenario and run tests
44+
run: |
45+
export XDG_RUNTIME_DIR="${RUNNER_TEMP}/xdg-runtime"
46+
bash scripts/prepare_runtime_scenario.sh "${VP_REAL_RUNTIME_SCENARIO}"
47+
python -m pytest -q tests/test_version_integration.py
48+
49+
- name: Clean up runtime scenario
50+
if: always()
51+
run: bash scripts/cleanup_runtime_scenario.sh
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
RUNNER_TEMP="${RUNNER_TEMP:-/tmp}"
5+
PODMAN_PID_FILE="${RUNNER_TEMP}/podman-service.pid"
6+
7+
if [[ -f "${PODMAN_PID_FILE}" ]]; then
8+
kill "$(cat "${PODMAN_PID_FILE}")" >/dev/null 2>&1 || true
9+
rm -f "${PODMAN_PID_FILE}"
10+
fi
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCENARIO="${1:?missing runtime scenario}"
5+
RUNNER_TEMP="${RUNNER_TEMP:-/tmp}"
6+
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-${RUNNER_TEMP}/xdg-runtime}"
7+
PODMAN_DIR="${XDG_RUNTIME_DIR}/podman"
8+
PODMAN_SOCKET_PATH="${PODMAN_DIR}/podman.sock"
9+
PODMAN_SOCKET_URL="unix://${PODMAN_SOCKET_PATH}"
10+
PODMAN_PID_FILE="${RUNNER_TEMP}/podman-service.pid"
11+
PODMAN_LOG_FILE="${RUNNER_TEMP}/podman-service.log"
12+
13+
wait_for_docker() {
14+
for _ in $(seq 1 30); do
15+
if docker version >/dev/null 2>&1; then
16+
return 0
17+
fi
18+
sleep 1
19+
done
20+
docker version
21+
}
22+
23+
wait_for_podman() {
24+
for _ in $(seq 1 30); do
25+
if [[ -S "${PODMAN_SOCKET_PATH}" ]] && podman --url "${PODMAN_SOCKET_URL}" version >/dev/null 2>&1; then
26+
return 0
27+
fi
28+
sleep 1
29+
done
30+
podman --url "${PODMAN_SOCKET_URL}" version
31+
}
32+
33+
ensure_docker_started() {
34+
if command -v systemctl >/dev/null 2>&1; then
35+
sudo systemctl start docker.socket || true
36+
sudo systemctl start docker.service || true
37+
fi
38+
if command -v service >/dev/null 2>&1; then
39+
sudo service docker start || true
40+
fi
41+
wait_for_docker
42+
}
43+
44+
stop_docker() {
45+
if command -v systemctl >/dev/null 2>&1; then
46+
sudo systemctl stop docker.service || true
47+
sudo systemctl stop docker.socket || true
48+
fi
49+
if command -v service >/dev/null 2>&1; then
50+
sudo service docker stop || true
51+
fi
52+
sleep 2
53+
}
54+
55+
install_podman() {
56+
sudo touch /etc/subuid /etc/subgid
57+
58+
if ! grep -q "^${USER}:" /etc/subuid; then
59+
echo "${USER}:100000:65536" | sudo tee -a /etc/subuid >/dev/null
60+
fi
61+
if ! grep -q "^${USER}:" /etc/subgid; then
62+
echo "${USER}:100000:65536" | sudo tee -a /etc/subgid >/dev/null
63+
fi
64+
65+
if command -v podman >/dev/null 2>&1; then
66+
return 0
67+
fi
68+
69+
sudo apt-get update
70+
sudo apt-get install -y podman
71+
}
72+
73+
stop_podman_service() {
74+
if [[ -f "${PODMAN_PID_FILE}" ]]; then
75+
kill "$(cat "${PODMAN_PID_FILE}")" >/dev/null 2>&1 || true
76+
rm -f "${PODMAN_PID_FILE}"
77+
fi
78+
rm -f "${PODMAN_SOCKET_PATH}"
79+
}
80+
81+
start_podman_service() {
82+
install_podman
83+
stop_podman_service
84+
85+
mkdir -p "${PODMAN_DIR}"
86+
chmod 700 "${XDG_RUNTIME_DIR}"
87+
88+
nohup podman system service --time=0 "${PODMAN_SOCKET_URL}" >"${PODMAN_LOG_FILE}" 2>&1 &
89+
echo "$!" >"${PODMAN_PID_FILE}"
90+
91+
wait_for_podman
92+
}
93+
94+
case "${SCENARIO}" in
95+
docker-only)
96+
stop_podman_service
97+
ensure_docker_started
98+
;;
99+
podman-only)
100+
start_podman_service
101+
stop_docker
102+
;;
103+
none)
104+
stop_podman_service
105+
stop_docker
106+
;;
107+
both-auto|both-default-docker|both-default-podman|both-switch)
108+
ensure_docker_started
109+
start_podman_service
110+
;;
111+
*)
112+
echo "Unknown runtime scenario: ${SCENARIO}" >&2
113+
exit 1
114+
;;
115+
esac
116+
117+
echo "Prepared runtime scenario: ${SCENARIO}"
118+
docker version >/dev/null 2>&1 && docker version --format '{{.Server.Version}}' || true
119+
command -v podman >/dev/null 2>&1 && podman --url "${PODMAN_SOCKET_URL}" version --format '{{.Server.Version}}' || true

tests/test_version_integration.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Real-runtime integration tests for `vp version`.
2+
3+
These tests are intended for dedicated CI jobs that prepare concrete host
4+
runtime scenarios. They are skipped unless ``VP_REAL_RUNTIME_SCENARIO`` is set.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import os
10+
import subprocess
11+
import sys
12+
from pathlib import Path
13+
14+
import pytest
15+
16+
SCENARIO = os.environ.get("VP_REAL_RUNTIME_SCENARIO")
17+
18+
if SCENARIO is None:
19+
pytest.skip(
20+
"requires dedicated runtime integration CI",
21+
allow_module_level=True,
22+
)
23+
24+
ROOT = Path(__file__).resolve().parents[1]
25+
SRC = ROOT / "src"
26+
27+
28+
def _run_vp(tmp_path: Path, *args: str) -> subprocess.CompletedProcess[str]:
29+
env = os.environ.copy()
30+
env["VP_CONFIG_DIR"] = str(tmp_path / "global-config")
31+
env["PYTHONPATH"] = (
32+
f"{SRC}{os.pathsep}{env['PYTHONPATH']}"
33+
if "PYTHONPATH" in env and env["PYTHONPATH"]
34+
else str(SRC)
35+
)
36+
env.pop("VP_CONTAINER_RUNTIME", None)
37+
return subprocess.run(
38+
[sys.executable, "-m", "vibepod.cli", *args],
39+
cwd=tmp_path,
40+
capture_output=True,
41+
text=True,
42+
check=False,
43+
env=env,
44+
timeout=60,
45+
)
46+
47+
48+
def _assert_ok(result: subprocess.CompletedProcess[str]) -> None:
49+
assert result.returncode == 0, (
50+
f"stdout:\n{result.stdout}\n"
51+
f"stderr:\n{result.stderr}"
52+
)
53+
54+
55+
def _runtime_parts(output: str) -> tuple[str, str]:
56+
for line in output.splitlines():
57+
if not line.startswith("Runtime:"):
58+
continue
59+
_, runtime_name, runtime_version = line.split(maxsplit=2)
60+
return runtime_name, runtime_version
61+
raise AssertionError(f"Missing Runtime line in output:\n{output}")
62+
63+
64+
def _assert_runtime(
65+
result: subprocess.CompletedProcess[str],
66+
*,
67+
runtime_name: str,
68+
) -> None:
69+
_assert_ok(result)
70+
assert "VibePod CLI:" in result.stdout
71+
assert "Python:" in result.stdout
72+
actual_runtime, actual_version = _runtime_parts(result.stdout)
73+
assert actual_runtime == runtime_name
74+
assert actual_version not in {"unknown", "unavailable"}
75+
76+
77+
def _assert_unavailable(result: subprocess.CompletedProcess[str]) -> None:
78+
_assert_ok(result)
79+
assert _runtime_parts(result.stdout) == ("unknown", "unavailable")
80+
81+
82+
def test_version_with_docker_only(tmp_path: Path) -> None:
83+
if SCENARIO != "docker-only":
84+
pytest.skip("scenario mismatch")
85+
86+
result = _run_vp(tmp_path, "version")
87+
88+
_assert_runtime(result, runtime_name="docker")
89+
90+
91+
def test_version_with_podman_only(tmp_path: Path) -> None:
92+
if SCENARIO != "podman-only":
93+
pytest.skip("scenario mismatch")
94+
95+
result = _run_vp(tmp_path, "version")
96+
97+
_assert_runtime(result, runtime_name="podman")
98+
99+
100+
def test_version_with_no_runtime_available(tmp_path: Path) -> None:
101+
if SCENARIO != "none":
102+
pytest.skip("scenario mismatch")
103+
104+
result = _run_vp(tmp_path, "version")
105+
106+
_assert_unavailable(result)
107+
108+
109+
def test_version_with_both_runtimes_defaults_to_docker_non_interactive(
110+
tmp_path: Path,
111+
) -> None:
112+
if SCENARIO != "both-auto":
113+
pytest.skip("scenario mismatch")
114+
115+
result = _run_vp(tmp_path, "version")
116+
117+
_assert_runtime(result, runtime_name="docker")
118+
119+
120+
def test_version_uses_saved_docker_default_when_both_runtimes_are_available(
121+
tmp_path: Path,
122+
) -> None:
123+
if SCENARIO != "both-default-docker":
124+
pytest.skip("scenario mismatch")
125+
126+
set_result = _run_vp(tmp_path, "config", "runtime", "docker")
127+
result = _run_vp(tmp_path, "version")
128+
129+
_assert_ok(set_result)
130+
assert "Set default container runtime to 'docker'" in set_result.stdout
131+
_assert_runtime(result, runtime_name="docker")
132+
133+
134+
def test_version_uses_saved_podman_default_when_both_runtimes_are_available(
135+
tmp_path: Path,
136+
) -> None:
137+
if SCENARIO != "both-default-podman":
138+
pytest.skip("scenario mismatch")
139+
140+
set_result = _run_vp(tmp_path, "config", "runtime", "podman")
141+
result = _run_vp(tmp_path, "version")
142+
143+
_assert_ok(set_result)
144+
assert "Set default container runtime to 'podman'" in set_result.stdout
145+
_assert_runtime(result, runtime_name="podman")
146+
147+
148+
def test_version_reflects_runtime_switch_command(tmp_path: Path) -> None:
149+
if SCENARIO != "both-switch":
150+
pytest.skip("scenario mismatch")
151+
152+
first_set_result = _run_vp(tmp_path, "config", "runtime", "docker")
153+
first_version_result = _run_vp(tmp_path, "version")
154+
second_set_result = _run_vp(tmp_path, "config", "runtime", "podman")
155+
second_version_result = _run_vp(tmp_path, "version")
156+
157+
_assert_ok(first_set_result)
158+
_assert_runtime(first_version_result, runtime_name="docker")
159+
_assert_ok(second_set_result)
160+
_assert_runtime(second_version_result, runtime_name="podman")

0 commit comments

Comments
 (0)