mirror of
https://github.com/krkn-chaos/krkn.git
synced 2026-09-01 09:37:16 +00:00
* test: migrate test_namespace.sh to CI/tests_v2 namespace_deletion Migrate the legacy CI/tests/test_namespace.sh functional test to the pytest-based v2 framework under CI/tests_v2/scenarios/namespace_deletion/. Covers the service_disruption (namespace deletion) scenario with happy-path cases (single-namespace object deletion, multi-namespace delete_count, multiple runs, wait_time handling, label-selector targeting) and negative cases (no-match regex, namespace/label mutual exclusion, delete_count exceeding available namespaces). - Add scenario assets (resource.yaml with Deployment+Service, scenario_base.yaml) - Register namespace_deletion marker in pytest.ini - Add namespace_deletion execution-evidence marker in lib/utils.py Closes #1405 * test: move namespace_deletion helpers into reusable lib modules Address review feedback: extract the per-test helpers into shared lib modules so other scenarios can reuse them. - lib/namespace.py: add POD_SECURITY_PRIVILEGED_LABELS, create_labeled_namespace, delete_namespace_quietly, and a make_namespace factory fixture (auto-cleanup) - lib/deploy.py: add deployment_exists, wait_for_no_deployment, wait_for_present_deployment_count, deploy_manifest_to_namespace - conftest.py: re-export make_namespace fixture - test_namespace_deletion.py: drop local helpers, use the lib functions/fixture * test: honor --keep-ns-on-fail in make_namespace factory The make_namespace finalizer always deleted ad-hoc namespaces, ignoring --keep-ns-on-fail on failure unlike test_namespace. Extract the keep-on-fail decision into a shared helper and use it in both fixtures so the documented debugging workflow works for scenarios that create extra namespaces. * test: surface FailToCreateError in deploy_manifest_to_namespace Mirror deploy_workload's error handling so manifest-apply failures in multi-namespace tests raise a formatted RuntimeError listing the underlying API exceptions instead of an opaque FailToCreateError. * test: clarify why label-selector test bypasses run_scenario The inline comment claimed run_scenario was bypassed because **overrides would collide with the positional namespace arg. The real reason is that NAMESPACE_IS_REGEX=True wraps an empty namespace as '^$', whereas label-selector mode needs a literal empty string. * test: use actual scenario inputs in negative-test failure context The no-match and mutual-exclusion tests reported context=namespace=self.ns (the ephemeral namespace), not the inputs actually under test. Reference the real namespace (and label_selector) so unexpected-success diagnostics are clear. * test: use non-default wait_time so override patching is exercised wait_time=30 matched the scenario_base.yaml default, so the test passed even if override patching regressed. Use wait_time=5 (non-default) so the override path is actually validated. * test: guard cluster post-checks under KRKN_TEST_DRY_RUN Three namespace_deletion tests asserted cluster side effects (workload deletion) after the Kraken run. Under KRKN_TEST_DRY_RUN=1 Kraken is skipped, so the seeded workload is never deleted and wait_for_no_deployment / wait_for_present_deployment_count would time out and fail. Guard those post-checks, and in test_label_selector_targeting (which bypasses run_scenario and calls run_kraken directly) honor dry-run explicitly by skipping the invocation and post-check. * test: tighten no-match assertion, clarify runs-loop test, dry-run-safe negatives Addresses Deep Code Review feedback on the namespace_deletion suite: - test_no_match_namespace_fails: drop the dead 'no namespaces matching' OR branch; the service_disruption plugin only ever logs 'not enough namespaces matching ...', so the assertion now checks that string directly. - test_multiple_runs_repeat_deletion -> test_multiple_runs_repeat_disruption_loop: rename + docstring make explicit that it verifies the outer runs loop iterates twice, not that object deletion recurs (Krkn does not redeploy between runs, so run 2 re-selects an already-empty namespace). Object removal is asserted in test_single_namespace_object_deletion. - Negative tests now return early under KRKN_TEST_DRY_RUN=1, since run_scenario returns a fake rc=0 and the failure path cannot be exercised; this makes the whole class consistent under make test-dry-run. * test: use UUID-based namespace in no-match test to avoid accidental matches * test: assert correct zero-match error in no-match namespace test A regex matching zero namespaces makes krkn_lib's check_namespaces raise 'there exists no namespaces matching' before the plugin's delete loop, so the 'not enough namespaces matching' branch is never reached for this case. Assert the actual zero-match message instead. * test: poll for async Service deletion in namespace_deletion test Kubernetes deletions are asynchronous, so checking the Service immediately after the scenario run could be flaky. Add wait_for_no_service (mirroring wait_for_no_deployment) and poll until the Service is actually gone before asserting. * test: assert Service deletion in label-selector namespace_deletion test test_label_selector_targeting deploys both a Deployment and a Service but only asserted the Deployment was removed. Add wait_for_no_service so the test fully validates that label-selector mode deletes all objects (including Services), matching test_single_namespace_object_deletion. Signed-off-by: augmentcode[bot] <185243770+augmentcode[bot]@users.noreply.github.com> * test: snapshot namespaces in wait_for_present_deployment_count list(namespaces) consumed the iterable before the poll loop re-iterated over the original, so a one-shot iterable (e.g. generator) would be empty on every poll. Snapshot to a list once and iterate over that snapshot. Signed-off-by: augmentcode[bot] <185243770+augmentcode[bot]@users.noreply.github.com> * ci: free up runner disk space before tests_v2 KinD run The Tests v2 (pytest functional) job intermittently fails with 'System.IO.IOException: No space left on device' on ubuntu-latest runners, which ship with only ~14GB free. Creating the KinD cluster plus pulling and kind-loading nginx:alpine and krkn:tools exhausts the disk, failing even the runner's own diagnostic logging. Reclaim ~20-30GB by removing the bundled .NET/Android/GHC SDKs and pruning docker images before the cluster is created. Signed-off-by: augmentcode[bot] <185243770+augmentcode[bot]@users.noreply.github.com> --------- Signed-off-by: augmentcode[bot] <185243770+augmentcode[bot]@users.noreply.github.com> Co-authored-by: augmentcode[bot] <185243770+augmentcode[bot]@users.noreply.github.com> Co-authored-by: Darshan Jain <darjain@redhat.com>
179 lines
6.6 KiB
Python
179 lines
6.6 KiB
Python
"""
|
|
Shared fixtures for pytest functional tests (CI/tests_v2).
|
|
Tests must be run from the repository root so run_kraken.py and config paths resolve.
|
|
"""
|
|
|
|
import html as html_lib
|
|
import logging
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Matches Krkn's log format "%(asctime)s [%(levelname)s] %(message)s" so each line can be
|
|
# rendered as a Timestamp/Level/Message row in the HTML report; non-matching lines (banner,
|
|
# tracebacks) fall back to a single Message cell.
|
|
_KRAKEN_LOG_LINE_RE = re.compile(
|
|
r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[,.]\d+)?)\s+\[(\w+)\]\s+(.*)$"
|
|
)
|
|
# Cap rows per run so a chatty scenario can't bloat the HTML report.
|
|
_KRAKEN_LOG_MAX_LINES = 1000
|
|
|
|
|
|
def pytest_addoption(parser):
|
|
parser.addoption(
|
|
"--keep-ns-on-fail",
|
|
action="store_true",
|
|
default=False,
|
|
help="Don't delete test namespaces on failure (for debugging)",
|
|
)
|
|
parser.addoption(
|
|
"--require-kind",
|
|
action="store_true",
|
|
default=False,
|
|
help="Skip tests unless current context is a known dev cluster (kind, minikube)",
|
|
)
|
|
|
|
|
|
def _kraken_log_html(outputs, evidence):
|
|
"""Render stashed Krkn stdout/stderr (and the evidence verdict) as an HTML block for the report."""
|
|
parts = ["<div><strong>Krkn Execution Log</strong></div>"]
|
|
for idx, out in enumerate(outputs, 1):
|
|
combined = ((out.get("stdout") or "") + "\n" + (out.get("stderr") or "")).strip("\n")
|
|
lines = combined.splitlines() if combined else []
|
|
if len(outputs) > 1:
|
|
parts.append(f"<div><em>Run {idx} (rc={out.get('returncode')})</em></div>")
|
|
if len(lines) > _KRAKEN_LOG_MAX_LINES:
|
|
parts.append(
|
|
f"<div><em>(showing last {_KRAKEN_LOG_MAX_LINES} of {len(lines)} lines)</em></div>"
|
|
)
|
|
lines = lines[-_KRAKEN_LOG_MAX_LINES:]
|
|
parts.append(
|
|
'<table style="border-collapse:collapse;font-family:monospace;font-size:12px">'
|
|
"<tr><th style='text-align:left;padding:2px 8px'>Timestamp</th>"
|
|
"<th style='text-align:left;padding:2px 8px'>Level</th>"
|
|
"<th style='text-align:left;padding:2px 8px'>Message</th></tr>"
|
|
)
|
|
for line in lines:
|
|
m = _KRAKEN_LOG_LINE_RE.match(line)
|
|
ts, lvl, msg = (m.group(1), m.group(2), m.group(3)) if m else ("", "", line)
|
|
parts.append(
|
|
"<tr>"
|
|
f"<td style='padding:2px 8px;white-space:nowrap'>{html_lib.escape(ts)}</td>"
|
|
f"<td style='padding:2px 8px'>{html_lib.escape(lvl)}</td>"
|
|
f"<td style='padding:2px 8px'>{html_lib.escape(msg)}</td>"
|
|
"</tr>"
|
|
)
|
|
parts.append("</table>")
|
|
if evidence is not None:
|
|
mark = "✓ Matched" if evidence["verified"] else "✗ No match for"
|
|
parts.append(
|
|
f"<div><strong>Execution Evidence:</strong> {mark} "
|
|
f"<code>{html_lib.escape(evidence['pattern'])}</code></div>"
|
|
)
|
|
return "".join(parts)
|
|
|
|
|
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
|
def pytest_runtest_makereport(item, call):
|
|
pytest_html = item.config.pluginmanager.getplugin("html")
|
|
outcome = yield
|
|
rep = outcome.get_result()
|
|
setattr(item, f"rep_{rep.when}", rep)
|
|
|
|
if rep.when != "call":
|
|
return
|
|
|
|
from lib.utils import EXECUTION_EVIDENCE # local import: lib is on path at runtime
|
|
|
|
evidence = EXECUTION_EVIDENCE.get(item.nodeid)
|
|
if evidence is not None:
|
|
# user_properties survive pytest-xdist worker -> controller transport so the
|
|
# terminal-summary hook (running on the controller) can build the table.
|
|
rep.user_properties.append(
|
|
("kraken_evidence", "verified" if evidence["verified"] else "missing")
|
|
)
|
|
|
|
outputs = getattr(item, "_kraken_outputs", None)
|
|
if pytest_html is not None and outputs:
|
|
extras = getattr(rep, "extras", [])
|
|
extras.append(pytest_html.extras.html(_kraken_log_html(outputs, evidence)))
|
|
rep.extras = extras
|
|
|
|
|
|
def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
|
"""Print and (in GitHub Actions) write the execution-evidence summary table."""
|
|
reports = terminalreporter.stats.get("passed", []) + terminalreporter.stats.get("failed", [])
|
|
rows = []
|
|
for rep in reports:
|
|
if getattr(rep, "when", None) != "call":
|
|
continue
|
|
evidence = None
|
|
for name, value in getattr(rep, "user_properties", []):
|
|
if name == "kraken_evidence":
|
|
evidence = value
|
|
if evidence == "verified":
|
|
ev = "✓ Verified"
|
|
elif evidence == "missing":
|
|
ev = "✗ No evidence"
|
|
else:
|
|
ev = "—"
|
|
result = "PASSED" if rep.passed else "FAILED"
|
|
rows.append((rep.nodeid, result, ev))
|
|
if not rows:
|
|
return
|
|
rows.sort()
|
|
terminalreporter.write_sep("=", "Execution Evidence Summary")
|
|
for nodeid, result, ev in rows:
|
|
terminalreporter.write_line(f"{result:<6} {ev:<14} {nodeid}")
|
|
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
if summary_path:
|
|
try:
|
|
with open(summary_path, "a") as f:
|
|
f.write("\n### Execution Evidence Summary\n\n")
|
|
f.write("| Test | Result | Execution Evidence |\n")
|
|
f.write("|------|--------|--------------------|\n")
|
|
for nodeid, result, ev in rows:
|
|
f.write(f"| `{nodeid}` | {result} | {ev} |\n")
|
|
except Exception as e:
|
|
logging.getLogger(__name__).warning(
|
|
"Could not write GITHUB_STEP_SUMMARY: %s", e
|
|
)
|
|
|
|
|
|
def _repo_root() -> Path:
|
|
"""Repository root (directory containing run_kraken.py and CI/)."""
|
|
return Path(__file__).resolve().parent.parent.parent
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def repo_root():
|
|
return _repo_root()
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def _configure_logging():
|
|
"""Set log format with timestamps for test runs."""
|
|
logging.basicConfig(
|
|
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
level=logging.INFO,
|
|
)
|
|
|
|
|
|
# Re-export fixtures from lib modules so pytest discovers them
|
|
from lib.deploy import deploy_workload, wait_for_pods_running # noqa: E402, F401
|
|
from lib.kraken import build_config, run_kraken, run_kraken_background # noqa: E402, F401
|
|
from lib.k8s import ( # noqa: E402, F401
|
|
_kube_config_loaded,
|
|
_log_cluster_context,
|
|
k8s_apps,
|
|
k8s_client,
|
|
k8s_core,
|
|
k8s_networking,
|
|
kubectl,
|
|
)
|
|
from lib.namespace import _cleanup_stale_namespaces, make_namespace, test_namespace # noqa: E402, F401
|
|
from lib.preflight import _preflight_checks # noqa: E402, F401
|