diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-01-08 18:03:47 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-02-09 03:32:52 +0000 |
| commit | dd13b2b6df049df622838aa3e5303ddbe1446fde (patch) | |
| tree | 07f1fe954162d2d7b236c7b9b448e11f8108b9f2 | |
| parent | 351930159f2b19f9371956e8278e00de89835357 (diff) | |
| download | meta-virtualization-dd13b2b6df049df622838aa3e5303ddbe1446fde.tar.gz | |
tests: add cleanup for orphan QEMU and stale test state
Add session-scoped autouse fixture that at session start:
1. Kills any QEMU processes holding ports used by tests (8080, 8081,
8888, etc.) - handles orphans from manual testing or crashed runs
2. Cleans up corrupt test state directories (docker-state.img with
"needs journal recovery") to ensure tests start fresh
This ensures tests don't fail due to leftover state from previous
runs or manual testing.
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
| -rw-r--r-- | tests/conftest.py | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/tests/conftest.py b/tests/conftest.py index 71b86430..49958879 100644 --- a/tests/conftest.py +++ b/tests/conftest.py | |||
| @@ -96,6 +96,51 @@ signal.signal(signal.SIGINT, _signal_handler) | |||
| 96 | signal.signal(signal.SIGTERM, _signal_handler) | 96 | signal.signal(signal.SIGTERM, _signal_handler) |
| 97 | 97 | ||
| 98 | 98 | ||
| 99 | # Ports used by tests that need to be free | ||
| 100 | TEST_PORTS = [8080, 8081, 8888, 8001, 8002, 9999, 7777, 6666] | ||
| 101 | |||
| 102 | |||
| 103 | def _cleanup_orphan_qemu_on_ports(): | ||
| 104 | """ | ||
| 105 | Kill any QEMU processes holding ports used by tests. | ||
| 106 | This handles cases where a previous test run or manual testing left | ||
| 107 | orphan QEMU processes that would block test port bindings. | ||
| 108 | """ | ||
| 109 | import re | ||
| 110 | |||
| 111 | try: | ||
| 112 | # Get listening sockets | ||
| 113 | result = subprocess.run( | ||
| 114 | ["ss", "-tlnp"], | ||
| 115 | capture_output=True, | ||
| 116 | text=True, | ||
| 117 | timeout=5 | ||
| 118 | ) | ||
| 119 | if result.returncode != 0: | ||
| 120 | return | ||
| 121 | |||
| 122 | for line in result.stdout.splitlines(): | ||
| 123 | # Check if any test port is in use | ||
| 124 | for port in TEST_PORTS: | ||
| 125 | if f":{port}" in line and "qemu" in line.lower(): | ||
| 126 | # Extract PID from ss output (format: users:(("qemu...",pid=12345,fd=...))) | ||
| 127 | match = re.search(r'pid=(\d+)', line) | ||
| 128 | if match: | ||
| 129 | pid = int(match.group(1)) | ||
| 130 | try: | ||
| 131 | os.kill(pid, signal.SIGTERM) | ||
| 132 | import time | ||
| 133 | time.sleep(0.5) | ||
| 134 | # Force kill if still running | ||
| 135 | if Path(f"/proc/{pid}").exists(): | ||
| 136 | os.kill(pid, signal.SIGKILL) | ||
| 137 | except (ProcessLookupError, PermissionError): | ||
| 138 | pass | ||
| 139 | break | ||
| 140 | except (subprocess.TimeoutExpired, FileNotFoundError): | ||
| 141 | pass | ||
| 142 | |||
| 143 | |||
| 99 | def pytest_addoption(parser): | 144 | def pytest_addoption(parser): |
| 100 | """Add custom command line options.""" | 145 | """Add custom command line options.""" |
| 101 | # vdkr/vpdmn options | 146 | # vdkr/vpdmn options |
| @@ -183,6 +228,59 @@ def pytest_addoption(parser): | |||
| 183 | ) | 228 | ) |
| 184 | 229 | ||
| 185 | 230 | ||
| 231 | def _cleanup_stale_test_state(): | ||
| 232 | """ | ||
| 233 | Clean up stale or corrupt test state directories. | ||
| 234 | This ensures tests start with a clean slate if previous runs crashed. | ||
| 235 | """ | ||
| 236 | for state_base in [TEST_STATE_BASE, VPDMN_TEST_STATE_BASE]: | ||
| 237 | state_path = Path(state_base) | ||
| 238 | if not state_path.exists(): | ||
| 239 | continue | ||
| 240 | |||
| 241 | for arch_dir in state_path.glob("*"): | ||
| 242 | if not arch_dir.is_dir(): | ||
| 243 | continue | ||
| 244 | |||
| 245 | docker_state = arch_dir / "docker-state.img" | ||
| 246 | daemon_pid = arch_dir / "daemon.pid" | ||
| 247 | |||
| 248 | # Check if daemon is actually running | ||
| 249 | daemon_running = False | ||
| 250 | if daemon_pid.exists(): | ||
| 251 | try: | ||
| 252 | pid = int(daemon_pid.read_text().strip()) | ||
| 253 | daemon_running = Path(f"/proc/{pid}").exists() | ||
| 254 | except (ValueError, OSError): | ||
| 255 | pass | ||
| 256 | |||
| 257 | # If daemon not running but state exists, it's stale - clean it | ||
| 258 | if not daemon_running and docker_state.exists(): | ||
| 259 | # Check if docker-state.img needs journal recovery (corrupt) | ||
| 260 | try: | ||
| 261 | result = subprocess.run( | ||
| 262 | ["file", str(docker_state)], | ||
| 263 | capture_output=True, | ||
| 264 | text=True, | ||
| 265 | timeout=5 | ||
| 266 | ) | ||
| 267 | if "needs journal recovery" in result.stdout: | ||
| 268 | # State is corrupt, clean it up | ||
| 269 | shutil.rmtree(arch_dir, ignore_errors=True) | ||
| 270 | except (subprocess.TimeoutExpired, FileNotFoundError): | ||
| 271 | pass | ||
| 272 | |||
| 273 | |||
| 274 | @pytest.fixture(scope="session", autouse=True) | ||
| 275 | def cleanup_orphan_qemu(): | ||
| 276 | """Clean up orphan QEMU processes and stale test state at session start.""" | ||
| 277 | _cleanup_orphan_qemu_on_ports() | ||
| 278 | _cleanup_stale_test_state() | ||
| 279 | yield | ||
| 280 | # Also clean up at end of session | ||
| 281 | _cleanup_orphan_qemu_on_ports() | ||
| 282 | |||
| 283 | |||
| 186 | @pytest.fixture(scope="session") | 284 | @pytest.fixture(scope="session") |
| 187 | def vdkr_dir(request): | 285 | def vdkr_dir(request): |
| 188 | """Path to vdkr standalone directory.""" | 286 | """Path to vdkr standalone directory.""" |
