mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-30 23:03:03 -07:00
enhance testing resilliancy
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
|||||||
.vscode-server
|
.vscode-server
|
||||||
.gitconfig
|
.gitconfig
|
||||||
.*CommandMarker
|
.*CommandMarker
|
||||||
|
.gemini/settings.json
|
||||||
deviceid
|
deviceid
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.cache
|
.cache
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ def _discover_host_addresses() -> tuple[str, ...]:
|
|||||||
HOST_ADDRESS_CANDIDATES = _discover_host_addresses()
|
HOST_ADDRESS_CANDIDATES = _discover_host_addresses()
|
||||||
LAST_PORT_SUCCESSES: dict[int, str] = {}
|
LAST_PORT_SUCCESSES: dict[int, str] = {}
|
||||||
|
|
||||||
|
# Test project name prefixes to clean up before runs
|
||||||
|
_TEST_PROJECT_PREFIXES = ("netalertx-missing", "netalertx-normal", "netalertx-custom", "netalertx-host", "netalertx-ram", "netalertx-dataloss")
|
||||||
|
|
||||||
pytestmark = [pytest.mark.docker, pytest.mark.compose]
|
pytestmark = [pytest.mark.docker, pytest.mark.compose]
|
||||||
|
|
||||||
IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
|
IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
|
||||||
@@ -400,6 +403,9 @@ def _run_docker_compose(
|
|||||||
post_up: Callable[[], None] | None = None,
|
post_up: Callable[[], None] | None = None,
|
||||||
) -> subprocess.CompletedProcess:
|
) -> subprocess.CompletedProcess:
|
||||||
"""Run docker compose up and capture output."""
|
"""Run docker compose up and capture output."""
|
||||||
|
# Clear global port tracking to prevent cross-test state pollution
|
||||||
|
LAST_PORT_SUCCESSES.clear()
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"docker", "compose",
|
"docker", "compose",
|
||||||
"-f", str(compose_file),
|
"-f", str(compose_file),
|
||||||
@@ -441,7 +447,7 @@ def _run_docker_compose(
|
|||||||
|
|
||||||
# Ensure no stale containers from previous runs; always clean before starting.
|
# Ensure no stale containers from previous runs; always clean before starting.
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
cmd + ["down", "-v"],
|
cmd + ["down", "-v", "--remove-orphans"],
|
||||||
cwd=compose_file.parent,
|
cwd=compose_file.parent,
|
||||||
stdout=sys.stdout,
|
stdout=sys.stdout,
|
||||||
stderr=sys.stderr,
|
stderr=sys.stderr,
|
||||||
@@ -450,6 +456,17 @@ def _run_docker_compose(
|
|||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Also clean up any orphaned containers from previous test runs with similar project prefixes
|
||||||
|
for prefix in _TEST_PROJECT_PREFIXES:
|
||||||
|
if project_name.startswith(prefix):
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "container", "prune", "-f", "--filter", f"label=com.docker.compose.project={project_name}"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
def _run_with_conflict_retry(run_cmd: list[str], run_timeout: int) -> subprocess.CompletedProcess:
|
def _run_with_conflict_retry(run_cmd: list[str], run_timeout: int) -> subprocess.CompletedProcess:
|
||||||
retry_conflict = True
|
retry_conflict = True
|
||||||
while True:
|
while True:
|
||||||
@@ -484,6 +501,39 @@ def _run_docker_compose(
|
|||||||
post_up_exc: BaseException | None = None
|
post_up_exc: BaseException | None = None
|
||||||
skip_exc: Skipped | None = None
|
skip_exc: Skipped | None = None
|
||||||
|
|
||||||
|
def _collect_logs_with_retry(max_attempts: int = 3, wait_between: float = 2.0) -> subprocess.CompletedProcess:
|
||||||
|
"""Collect logs with retry to handle timing races where container hasn't flushed output yet."""
|
||||||
|
logs_cmd = cmd + ["logs"]
|
||||||
|
best_result: subprocess.CompletedProcess | None = None
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
print(f"Running logs cmd (attempt {attempt + 1}/{max_attempts}): {logs_cmd}")
|
||||||
|
logs_result = subprocess.run(
|
||||||
|
logs_cmd,
|
||||||
|
cwd=compose_file.parent,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout + 5,
|
||||||
|
check=False,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
print(logs_result.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
|
||||||
|
print(logs_result.stderr) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
|
||||||
|
|
||||||
|
combined = (logs_result.stdout or "") + (logs_result.stderr or "")
|
||||||
|
# Keep the result with the most content
|
||||||
|
if best_result is None or len(combined) > len((best_result.stdout or "") + (best_result.stderr or "")):
|
||||||
|
best_result = logs_result
|
||||||
|
|
||||||
|
# If we see the expected startup marker, logs are complete
|
||||||
|
if "Startup pre-checks" in combined or "NETALERTX_CHECK_ONLY" in combined:
|
||||||
|
break
|
||||||
|
|
||||||
|
if attempt < max_attempts - 1:
|
||||||
|
time.sleep(wait_between)
|
||||||
|
|
||||||
|
return best_result or logs_result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if detached:
|
if detached:
|
||||||
up_result = _run_with_conflict_retry(up_cmd, timeout)
|
up_result = _run_with_conflict_retry(up_cmd, timeout)
|
||||||
@@ -496,20 +546,7 @@ def _run_docker_compose(
|
|||||||
except BaseException as exc: # noqa: BLE001 - bubble the root cause through the result payload
|
except BaseException as exc: # noqa: BLE001 - bubble the root cause through the result payload
|
||||||
post_up_exc = exc
|
post_up_exc = exc
|
||||||
|
|
||||||
logs_cmd = cmd + ["logs"]
|
logs_result = _collect_logs_with_retry()
|
||||||
print(f"Running logs cmd: {logs_cmd}")
|
|
||||||
logs_result = subprocess.run(
|
|
||||||
logs_cmd,
|
|
||||||
cwd=compose_file.parent,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
timeout=timeout,
|
|
||||||
check=False,
|
|
||||||
env=env,
|
|
||||||
)
|
|
||||||
print(logs_result.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
|
|
||||||
print(logs_result.stderr) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
|
|
||||||
|
|
||||||
result = subprocess.CompletedProcess(
|
result = subprocess.CompletedProcess(
|
||||||
up_cmd,
|
up_cmd,
|
||||||
@@ -520,20 +557,7 @@ def _run_docker_compose(
|
|||||||
else:
|
else:
|
||||||
up_result = _run_with_conflict_retry(up_cmd, timeout + 10)
|
up_result = _run_with_conflict_retry(up_cmd, timeout + 10)
|
||||||
|
|
||||||
logs_cmd = cmd + ["logs"]
|
logs_result = _collect_logs_with_retry()
|
||||||
print(f"Running logs cmd: {logs_cmd}")
|
|
||||||
logs_result = subprocess.run(
|
|
||||||
logs_cmd,
|
|
||||||
cwd=compose_file.parent,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
timeout=timeout + 10,
|
|
||||||
check=False,
|
|
||||||
env=env,
|
|
||||||
)
|
|
||||||
print(logs_result.stdout) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
|
|
||||||
print(logs_result.stderr) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
|
|
||||||
|
|
||||||
result = subprocess.CompletedProcess(
|
result = subprocess.CompletedProcess(
|
||||||
up_cmd,
|
up_cmd,
|
||||||
@@ -640,7 +664,7 @@ def _run_docker_compose(
|
|||||||
# additional attributes (`output`, `post_up_error`, etc.). Overwriting it
|
# additional attributes (`output`, `post_up_error`, etc.). Overwriting it
|
||||||
# caused callers to see a CompletedProcess without `output` -> AttributeError.
|
# caused callers to see a CompletedProcess without `output` -> AttributeError.
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["docker", "compose", "-f", str(compose_file), "-p", project_name, "down", "-v"],
|
["docker", "compose", "-f", str(compose_file), "-p", project_name, "down", "-v", "--remove-orphans"],
|
||||||
cwd=compose_file.parent,
|
cwd=compose_file.parent,
|
||||||
stdout=sys.stdout,
|
stdout=sys.stdout,
|
||||||
stderr=sys.stderr,
|
stderr=sys.stderr,
|
||||||
@@ -649,6 +673,14 @@ def _run_docker_compose(
|
|||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Prune any dangling volumes from this project to prevent state leakage
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "volume", "prune", "-f", "--filter", f"label=com.docker.compose.project={project_name}"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
for proc in audit_streams:
|
for proc in audit_streams:
|
||||||
try:
|
try:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
@@ -751,7 +783,6 @@ def test_custom_port_with_unwritable_nginx_config_compose() -> None:
|
|||||||
assert "unable to write to /tmp/nginx/active-config/netalertx.conf" in lowered_output
|
assert "unable to write to /tmp/nginx/active-config/netalertx.conf" in lowered_output
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_host_network_compose(tmp_path: pathlib.Path) -> None:
|
def test_host_network_compose(tmp_path: pathlib.Path) -> None:
|
||||||
"""Test host networking mode using docker compose.
|
"""Test host networking mode using docker compose.
|
||||||
|
|
||||||
|
|||||||
@@ -12,26 +12,46 @@ def test_run_docker_compose_returns_output(monkeypatch, tmp_path):
|
|||||||
compose_file = tmp_path / "docker-compose.yml"
|
compose_file = tmp_path / "docker-compose.yml"
|
||||||
compose_file.write_text("services: {}")
|
compose_file.write_text("services: {}")
|
||||||
|
|
||||||
# Prepare a sequence of CompletedProcess objects to be returned by fake `run`
|
# Track calls to identify what's being run
|
||||||
cps = [
|
call_log = []
|
||||||
subprocess.CompletedProcess([], 0, stdout="down-initial\n", stderr=""),
|
|
||||||
subprocess.CompletedProcess(["up"], 0, stdout="up-out\n", stderr=""),
|
|
||||||
subprocess.CompletedProcess(["logs"], 0, stdout="log-out\n", stderr=""),
|
|
||||||
# ps_proc: return valid container entries
|
|
||||||
subprocess.CompletedProcess(["ps"], 0, stdout="test-container Running 0\n", stderr=""),
|
|
||||||
subprocess.CompletedProcess([], 0, stdout="down-final\n", stderr=""),
|
|
||||||
]
|
|
||||||
|
|
||||||
def fake_run(*_, **__):
|
def fake_run(cmd, *args, **kwargs):
|
||||||
try:
|
"""Return appropriate fake responses based on the command being run."""
|
||||||
return cps.pop(0)
|
cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
|
||||||
except IndexError:
|
call_log.append(cmd_str)
|
||||||
# Safety: return a harmless CompletedProcess
|
|
||||||
return subprocess.CompletedProcess([], 0, stdout="", stderr="")
|
# Identify the command type and return appropriate response
|
||||||
|
if "down" in cmd_str:
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, stdout="down-out\n", stderr="")
|
||||||
|
elif "volume prune" in cmd_str:
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||||
|
elif "container prune" in cmd_str:
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||||
|
elif "ps" in cmd_str:
|
||||||
|
# Return valid container entries with "Startup pre-checks" won't be here
|
||||||
|
# but we need valid ps output
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, stdout="test-container Running 0\n", stderr="")
|
||||||
|
elif "logs" in cmd_str:
|
||||||
|
# Include "Startup pre-checks" so the retry logic exits early
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, stdout="log-out\nStartup pre-checks\n", stderr="")
|
||||||
|
elif "up" in cmd_str:
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, stdout="up-out\n", stderr="")
|
||||||
|
else:
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, stdout="", stderr="")
|
||||||
|
|
||||||
# Monkeypatch subprocess.run used inside the module
|
# Monkeypatch subprocess.run used inside the module
|
||||||
monkeypatch.setattr(mod.subprocess, "run", fake_run)
|
monkeypatch.setattr(mod.subprocess, "run", fake_run)
|
||||||
|
|
||||||
|
# Also patch subprocess.Popen for the audit stream (returns immediately terminating proc)
|
||||||
|
class FakePopen:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setattr(mod.subprocess, "Popen", FakePopen)
|
||||||
|
|
||||||
# Call under test
|
# Call under test
|
||||||
result = mod._run_docker_compose(compose_file, "proj-test", timeout=1, detached=False)
|
result = mod._run_docker_compose(compose_file, "proj-test", timeout=1, detached=False)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user