97 lines
3.3 KiB
Python
97 lines
3.3 KiB
Python
|
|
"""Tests for detecting Jenkins builds that never reach a terminal result."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from ariadne.services import hermes_hung_builds as module
|
||
|
|
|
||
|
|
|
||
|
|
NOW = 1_800_000_000.0
|
||
|
|
_MINUTE_MS = 60_000
|
||
|
|
|
||
|
|
|
||
|
|
def _build(minutes_ago: float, building: bool = True, **overrides): # type: ignore[no-untyped-def]
|
||
|
|
payload = {
|
||
|
|
"number": 272,
|
||
|
|
"building": building,
|
||
|
|
"timestamp": int((NOW - minutes_ago * 60) * 1000),
|
||
|
|
"url": "https://ci.example/job/metis/272/",
|
||
|
|
}
|
||
|
|
payload.update(overrides)
|
||
|
|
return payload
|
||
|
|
|
||
|
|
|
||
|
|
def test_elapsed_minutes_measures_from_the_start_timestamp() -> None:
|
||
|
|
assert module.elapsed_minutes(_build(75), now=NOW) == 75.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_elapsed_minutes_treats_an_unreadable_start_as_zero() -> None:
|
||
|
|
"""An unreadable timestamp must never look like a hung build."""
|
||
|
|
|
||
|
|
assert module.elapsed_minutes({}, now=NOW) == 0.0
|
||
|
|
assert module.elapsed_minutes({"timestamp": None}, now=NOW) == 0.0
|
||
|
|
assert module.elapsed_minutes({"timestamp": "not-a-number"}, now=NOW) == 0.0
|
||
|
|
assert module.elapsed_minutes({"timestamp": 0}, now=NOW) == 0.0
|
||
|
|
assert module.elapsed_minutes({"timestamp": -5}, now=NOW) == 0.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_elapsed_minutes_never_goes_negative() -> None:
|
||
|
|
"""A clock skewed into the future must not produce a negative age."""
|
||
|
|
|
||
|
|
assert module.elapsed_minutes(_build(-30), now=NOW) == 0.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_elapsed_minutes_defaults_to_the_wall_clock() -> None:
|
||
|
|
assert module.elapsed_minutes(_build(0.0, timestamp=1)) > 0.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_running_build_past_the_cap_is_hung() -> None:
|
||
|
|
assert module.is_hung(_build(75), 45.0, now=NOW) is True
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_running_build_within_the_cap_is_not_hung() -> None:
|
||
|
|
assert module.is_hung(_build(10), 45.0, now=NOW) is False
|
||
|
|
assert module.is_hung(_build(45), 45.0, now=NOW) is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_finished_build_is_never_hung() -> None:
|
||
|
|
"""Terminal builds are the normal path's business, not this one."""
|
||
|
|
|
||
|
|
assert module.is_hung(_build(600, building=False), 45.0, now=NOW) is False
|
||
|
|
assert module.is_hung({"timestamp": 1}, 45.0, now=NOW) is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_non_positive_cap_disables_the_check() -> None:
|
||
|
|
assert module.is_hung(_build(6000), 0.0, now=NOW) is False
|
||
|
|
assert module.is_hung(_build(6000), -1.0, now=NOW) is False
|
||
|
|
|
||
|
|
|
||
|
|
def test_hung_bundle_carries_only_observed_facts() -> None:
|
||
|
|
bundle = module.hung_bundle("metis", _build(75), 45.0)
|
||
|
|
jenkins = bundle["jenkins"]
|
||
|
|
|
||
|
|
assert jenkins["job"] == "metis"
|
||
|
|
assert jenkins["build_number"] == 272
|
||
|
|
assert jenkins["building"] is True
|
||
|
|
assert jenkins["cap_minutes"] == 45.0
|
||
|
|
assert jenkins["console_failures"] == []
|
||
|
|
assert bundle["log_evidence"] == {"records": []}
|
||
|
|
assert "has been running for" in jenkins["console_tail"]
|
||
|
|
assert "holding a Jenkins agent slot" in jenkins["console_tail"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_hung_bundle_survives_a_build_with_nothing_in_it() -> None:
|
||
|
|
"""Bundle construction must not raise on a sparse Jenkins payload."""
|
||
|
|
|
||
|
|
bundle = module.hung_bundle("metis", {}, 45.0)
|
||
|
|
|
||
|
|
assert bundle["jenkins"]["build_number"] is None
|
||
|
|
assert bundle["jenkins"]["url"] == ""
|
||
|
|
assert bundle["jenkins"]["elapsed_minutes"] == 0.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_reason_and_classification_are_stable_labels() -> None:
|
||
|
|
"""These land in issue titles and event details, so they are frozen."""
|
||
|
|
|
||
|
|
assert module.HUNG_REASON == "hung_build"
|
||
|
|
assert module.HUNG_CLASSIFICATION == "build_exceeded_time_cap"
|