test: cover ariadne triage evidence helpers
This commit is contained in:
parent
69a5baa955
commit
38a5e924fe
@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from ariadne.services import testing_triage
|
||||
|
||||
|
||||
@ -36,6 +38,18 @@ class DummyStorage:
|
||||
return matching[-limit:][::-1]
|
||||
|
||||
|
||||
class SettingsStub:
|
||||
def __init__(self, **overrides) -> None: # type: ignore[no-untyped-def]
|
||||
self.vm_url = ""
|
||||
self.cluster_state_vm_timeout_sec = 1.0
|
||||
self.jenkins_base_url = ""
|
||||
self.jenkins_api_user = ""
|
||||
self.jenkins_api_token = ""
|
||||
self.jenkins_api_timeout_sec = 1.0
|
||||
for key, value in overrides.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
def test_collect_testing_triage_builds_bundle(monkeypatch) -> None:
|
||||
storage = DummyStorage()
|
||||
monkeypatch.setattr(
|
||||
@ -72,3 +86,381 @@ def test_run_testing_triage_stores_latest(monkeypatch) -> None:
|
||||
assert summary.status == "ok"
|
||||
assert storage.events[0][0] == testing_triage.TRIAGE_EVENT_TYPE
|
||||
assert latest["summary"]["status"] == "ok"
|
||||
|
||||
|
||||
def test_latest_testing_triage_bundle_handles_json_strings() -> None:
|
||||
class JsonStorage:
|
||||
def list_events(self, limit: int = 1, event_type: str | None = None): # type: ignore[no-untyped-def]
|
||||
assert limit == 1
|
||||
assert event_type == testing_triage.TRIAGE_EVENT_TYPE
|
||||
return [{"detail": json.dumps({"summary": {"status": "ok"}})}]
|
||||
|
||||
latest = testing_triage.latest_testing_triage_bundle(JsonStorage()) # type: ignore[arg-type]
|
||||
|
||||
assert latest == {"summary": {"status": "ok"}}
|
||||
|
||||
|
||||
def test_latest_testing_triage_bundle_ignores_bad_payloads() -> None:
|
||||
class BadStorage:
|
||||
def __init__(self, detail) -> None: # type: ignore[no-untyped-def]
|
||||
self.detail = detail
|
||||
|
||||
def list_events(self, limit: int = 1, event_type: str | None = None): # type: ignore[no-untyped-def]
|
||||
return [{"detail": self.detail}]
|
||||
|
||||
assert testing_triage.latest_testing_triage_bundle(BadStorage("{")) is None # type: ignore[arg-type]
|
||||
assert testing_triage.latest_testing_triage_bundle(BadStorage(["nope"])) is None # type: ignore[arg-type]
|
||||
assert testing_triage.latest_testing_triage_bundle(BadStorage(None)) is None # type: ignore[arg-type]
|
||||
|
||||
class EmptyStorage:
|
||||
def list_events(self, limit: int = 1, event_type: str | None = None): # type: ignore[no-untyped-def]
|
||||
return []
|
||||
|
||||
assert testing_triage.latest_testing_triage_bundle(EmptyStorage()) is None # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_latest_cluster_snapshot_falls_back_to_live_collect(monkeypatch) -> None:
|
||||
class BrokenStorage:
|
||||
def latest_cluster_state(self): # type: ignore[no-untyped-def]
|
||||
raise RuntimeError("storage down")
|
||||
|
||||
monkeypatch.setattr(
|
||||
testing_triage,
|
||||
"collect_cluster_state",
|
||||
lambda: ({"collected_at": "live"}, {"status": "ok"}),
|
||||
)
|
||||
errors: list[str] = []
|
||||
|
||||
snapshot = testing_triage._latest_cluster_snapshot(BrokenStorage(), errors) # noqa: SLF001
|
||||
|
||||
assert snapshot == {"collected_at": "live"}
|
||||
assert errors == ["cluster_state_latest: storage down"]
|
||||
|
||||
|
||||
def test_latest_cluster_snapshot_records_collect_error(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
testing_triage,
|
||||
"collect_cluster_state",
|
||||
lambda: (_ for _ in ()).throw(RuntimeError("api down")),
|
||||
)
|
||||
errors: list[str] = []
|
||||
|
||||
snapshot = testing_triage._latest_cluster_snapshot(None, errors) # noqa: SLF001
|
||||
|
||||
assert snapshot == {}
|
||||
assert errors == ["cluster_state_collect: api down"]
|
||||
|
||||
|
||||
def test_cluster_evidence_limits_and_defaults() -> None:
|
||||
snapshot = {
|
||||
"summary": {
|
||||
"health_bullets": list(range(20)),
|
||||
"attention_ranked": [{"item": i} for i in range(20)],
|
||||
},
|
||||
"nodes_summary": {"total": 2, "ready": 1, "not_ready": 1, "not_ready_names": ["titan-06"]},
|
||||
"flux": {"items": [{"name": str(i)} for i in range(20)]},
|
||||
"pod_issues": {
|
||||
"items": [{"pod": str(i)} for i in range(20)],
|
||||
"pending_oldest": [{"pod": "p"}],
|
||||
},
|
||||
"jobs": {"failing": [{"job": "j"}], "active_oldest": [{"job": "old"}]},
|
||||
"events": {"warnings_recent": [{"message": "warn"}]},
|
||||
}
|
||||
|
||||
evidence = testing_triage._cluster_evidence(snapshot) # noqa: SLF001
|
||||
|
||||
assert len(evidence["health_bullets"]) == testing_triage._MAX_EVIDENCE_ITEMS # noqa: SLF001
|
||||
assert evidence["nodes"]["not_ready_names"] == ["titan-06"]
|
||||
assert evidence["jobs_failing"] == [{"job": "j"}]
|
||||
|
||||
|
||||
def test_vm_items_handles_success_failure_and_bad_values(monkeypatch) -> None:
|
||||
class FakeResponse:
|
||||
def __init__(self, payload) -> None: # type: ignore[no-untyped-def]
|
||||
self.payload = payload
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
return None
|
||||
|
||||
def json(self): # type: ignore[no-untyped-def]
|
||||
return self.payload
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
||||
self.payload = kwargs.pop("payload", None)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def get(self, url, params=None): # type: ignore[no-untyped-def]
|
||||
assert url.endswith("/api/v1/query")
|
||||
assert params == {"query": "up"}
|
||||
return FakeResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"result": [
|
||||
{"metric": {"suite": "ariadne", "__name__": "ignored"}, "value": [1, "2.5"]},
|
||||
{"metric": {"suite": "metis"}, "value": [1, "bad"]},
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(testing_triage, "settings", SettingsStub(vm_url="http://vm"))
|
||||
monkeypatch.setattr(testing_triage.httpx, "Client", FakeClient)
|
||||
errors: list[str] = []
|
||||
|
||||
items = testing_triage._vm_items("up", errors) # noqa: SLF001
|
||||
|
||||
assert items == [
|
||||
{"labels": {"suite": "ariadne"}, "value": 2.5},
|
||||
{"labels": {"suite": "metis"}, "value": 0.0},
|
||||
]
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_vm_items_records_errors(monkeypatch) -> None:
|
||||
class BrokenClient:
|
||||
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def __enter__(self):
|
||||
raise RuntimeError("network down")
|
||||
|
||||
def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(testing_triage, "settings", SettingsStub(vm_url="http://vm"))
|
||||
monkeypatch.setattr(testing_triage.httpx, "Client", BrokenClient)
|
||||
errors: list[str] = []
|
||||
|
||||
assert testing_triage._vm_items("up", errors) == [] # noqa: SLF001
|
||||
assert errors == ["victoria_metrics: network down"]
|
||||
|
||||
|
||||
def test_vm_items_handles_disabled_and_query_failure(monkeypatch) -> None:
|
||||
class FailedResponse:
|
||||
def raise_for_status(self) -> None:
|
||||
return None
|
||||
|
||||
def json(self): # type: ignore[no-untyped-def]
|
||||
return {"status": "error"}
|
||||
|
||||
class FailedClient:
|
||||
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def get(self, url, params=None): # type: ignore[no-untyped-def]
|
||||
return FailedResponse()
|
||||
|
||||
errors: list[str] = []
|
||||
monkeypatch.setattr(testing_triage, "settings", SettingsStub(vm_url=""))
|
||||
assert testing_triage._vm_items("up", errors) == [] # noqa: SLF001
|
||||
assert errors == []
|
||||
|
||||
monkeypatch.setattr(testing_triage, "settings", SettingsStub(vm_url="http://vm"))
|
||||
monkeypatch.setattr(testing_triage.httpx, "Client", FailedClient)
|
||||
assert testing_triage._vm_items("up", errors) == [] # noqa: SLF001
|
||||
assert errors == ["victoria_metrics: query failed"]
|
||||
|
||||
|
||||
def test_jenkins_flatten_status_and_log_tail(monkeypatch) -> None:
|
||||
rows = [
|
||||
"skip",
|
||||
{"name": "", "lastBuild": {}},
|
||||
{
|
||||
"name": "folder",
|
||||
"jobs": [
|
||||
{
|
||||
"name": "child",
|
||||
"url": "http://jenkins/job/folder/job/child/",
|
||||
"color": "red",
|
||||
"lastBuild": {
|
||||
"number": 7,
|
||||
"result": "FAILURE",
|
||||
"timestamp": 2000,
|
||||
"duration": 3000,
|
||||
"url": "http://jenkins/build/7/",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"name": "empty", "jobs": []},
|
||||
]
|
||||
|
||||
flattened = testing_triage._flatten_jobs(rows) # noqa: SLF001
|
||||
job = testing_triage._jenkins_job(flattened[0]) # noqa: SLF001
|
||||
|
||||
assert flattened[0]["name"] == "folder/child"
|
||||
assert job is not None
|
||||
assert job["status"] == "failure"
|
||||
assert job["last_run_ts"] == 2.0
|
||||
assert job["last_duration_seconds"] == 3.0
|
||||
assert job["console_url"] == "http://jenkins/build/7/consoleText"
|
||||
assert testing_triage._jenkins_status({"color": "blue_anime"}, "") == "running" # noqa: SLF001
|
||||
assert testing_triage._jenkins_status({}, "SUCCESS") == "success" # noqa: SLF001
|
||||
assert testing_triage._jenkins_status({"color": "green"}, "") == "success" # noqa: SLF001
|
||||
assert testing_triage._jenkins_status({"color": "yellow"}, "") == "failure" # noqa: SLF001
|
||||
assert testing_triage._jenkins_status({"color": "grey"}, "") == "unknown" # noqa: SLF001
|
||||
assert testing_triage._jenkins_job({"name": 1, "url": "u"}) is None # noqa: SLF001
|
||||
long_tail = testing_triage._tail_text("x" * (testing_triage._MAX_JENKINS_LOG_CHARS + 10)) # noqa: SLF001
|
||||
assert len(long_tail) == testing_triage._MAX_JENKINS_LOG_CHARS # noqa: SLF001
|
||||
assert testing_triage._tail_text("\n".join(str(i) for i in range(100))).startswith("20\n") # noqa: SLF001
|
||||
|
||||
|
||||
def test_attach_jenkins_log_tail_ignores_missing_url_and_records_errors(monkeypatch) -> None:
|
||||
class BrokenClient:
|
||||
def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def get(self, url): # type: ignore[no-untyped-def]
|
||||
raise RuntimeError("log gone")
|
||||
|
||||
errors: list[str] = []
|
||||
testing_triage._attach_jenkins_log_tail({"job": "missing"}, errors) # noqa: SLF001
|
||||
assert errors == []
|
||||
|
||||
monkeypatch.setattr(testing_triage, "settings", SettingsStub(jenkins_api_timeout_sec=1))
|
||||
monkeypatch.setattr(testing_triage.httpx, "Client", BrokenClient)
|
||||
testing_triage._attach_jenkins_log_tail({"job": "ariadne", "console_url": "http://jenkins/log"}, errors) # noqa: SLF001
|
||||
|
||||
assert errors == ["jenkins_log:ariadne: log gone"]
|
||||
|
||||
|
||||
def test_fetch_jenkins_jobs_and_log_tail(monkeypatch) -> None:
|
||||
class FakeResponse:
|
||||
def __init__(self, payload=None, text="") -> None: # type: ignore[no-untyped-def]
|
||||
self.payload = payload
|
||||
self.text = text
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
return None
|
||||
|
||||
def json(self): # type: ignore[no-untyped-def]
|
||||
return self.payload
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def]
|
||||
assert kwargs["auth"] == ("jenkins", "token")
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
|
||||
return None
|
||||
|
||||
def get(self, url, params=None): # type: ignore[no-untyped-def]
|
||||
if url.endswith("/api/json"):
|
||||
assert "tree" in params
|
||||
return FakeResponse(
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"name": "ariadne",
|
||||
"url": "http://jenkins/job/ariadne/",
|
||||
"color": "red",
|
||||
"lastBuild": {"number": 8, "result": "FAILURE", "timestamp": 1000, "duration": 1000},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
return FakeResponse(text="line1\nline2")
|
||||
|
||||
monkeypatch.setattr(
|
||||
testing_triage,
|
||||
"settings",
|
||||
SettingsStub(jenkins_api_user="jenkins", jenkins_api_token="token", jenkins_api_timeout_sec=3),
|
||||
)
|
||||
monkeypatch.setattr(testing_triage.httpx, "Client", FakeClient)
|
||||
|
||||
jobs = testing_triage._fetch_jenkins_jobs("http://jenkins") # noqa: SLF001
|
||||
errors: list[str] = []
|
||||
testing_triage._attach_jenkins_log_tail(jobs[0], errors) # noqa: SLF001
|
||||
|
||||
assert jobs[0]["job"] == "ariadne"
|
||||
assert jobs[0]["status"] == "failure"
|
||||
assert jobs[0]["log_tail"] == "line1\nline2"
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_jenkins_signals_handles_disabled_and_failures(monkeypatch) -> None:
|
||||
monkeypatch.setattr(testing_triage, "settings", SettingsStub(jenkins_base_url=""))
|
||||
assert testing_triage._jenkins_signals([]) == {"failed_builds": []} # noqa: SLF001
|
||||
|
||||
monkeypatch.setattr(testing_triage, "settings", SettingsStub(jenkins_base_url="http://jenkins"))
|
||||
monkeypatch.setattr(
|
||||
testing_triage,
|
||||
"_fetch_jenkins_jobs",
|
||||
lambda base_url: (_ for _ in ()).throw(RuntimeError("boom")),
|
||||
)
|
||||
errors: list[str] = []
|
||||
|
||||
assert testing_triage._jenkins_signals(errors) == {"failed_builds": []} # noqa: SLF001
|
||||
assert errors == ["jenkins: boom"]
|
||||
|
||||
|
||||
def test_jenkins_signals_attaches_recent_failed_builds(monkeypatch) -> None:
|
||||
jobs = [
|
||||
{"job": "old", "status": "failure", "last_run_ts": 1},
|
||||
{"job": "ok", "status": "success", "last_run_ts": 5},
|
||||
{"job": "running", "status": "running", "last_run_ts": 10},
|
||||
{"job": "unknown", "status": "unknown", "last_run_ts": 3},
|
||||
]
|
||||
attached: list[str] = []
|
||||
monkeypatch.setattr(testing_triage, "settings", SettingsStub(jenkins_base_url="http://jenkins"))
|
||||
monkeypatch.setattr(testing_triage, "_fetch_jenkins_jobs", lambda base_url: jobs)
|
||||
monkeypatch.setattr(testing_triage, "_attach_jenkins_log_tail", lambda job, errors: attached.append(job["job"]))
|
||||
|
||||
signals = testing_triage._jenkins_signals([]) # noqa: SLF001
|
||||
|
||||
assert [item["job"] for item in signals["failed_builds"]] == ["running", "unknown", "old"]
|
||||
assert attached == ["running", "unknown", "old"]
|
||||
|
||||
|
||||
def test_summary_and_markdown_helpers() -> None:
|
||||
quality = {
|
||||
"failed_runs_24h": {"items": [{"labels": {"suite": "ariadne"}, "value": 2}]},
|
||||
"empty": {"items": []},
|
||||
"bad": "skip",
|
||||
}
|
||||
cluster = {
|
||||
"flux_not_ready": [{"name": "monitoring"}],
|
||||
"pod_issues": [],
|
||||
"jobs_failing": [{"job": "job"}],
|
||||
"collected_at": "now",
|
||||
}
|
||||
jenkins = {"failed_builds": [{"job": "ariadne"}]}
|
||||
|
||||
summary = testing_triage._summary(cluster, quality, jenkins, []) # noqa: SLF001
|
||||
markdown = testing_triage._render_markdown( # noqa: SLF001
|
||||
{
|
||||
"generated_at": "now",
|
||||
"summary": summary,
|
||||
"evidence": {"cluster": cluster, "quality": quality, "jenkins": jenkins},
|
||||
"unknowns": ["missing vm"],
|
||||
}
|
||||
)
|
||||
|
||||
assert summary["status"] == "needs_attention"
|
||||
assert summary["failed_suites"] == ["ariadne"]
|
||||
assert "- Flux: monitoring" in markdown
|
||||
assert "- failed_runs_24h: {'suite': 'ariadne'} value=2" in markdown
|
||||
assert "- missing vm" in markdown
|
||||
assert testing_triage._markdown_named_items("Pods", ["bad"], "pod") == ["- Pods: none"] # noqa: SLF001
|
||||
assert testing_triage._jenkins_auth() is None # noqa: SLF001
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user