Add per-spec fallback for categorized watcher grouped summaries

- update the categorized watcher to fall back to earlier per-spec Run Finished entries when a grouped Cloud Run Finished section is present but does not contain host rows
- broaden duration parsing so colon-form timings like 13:59 and 15:32 from per-spec Cypress summaries are converted into seconds correctly
- make grouped Mattermost posting resilient across categorized batches whose cloud-summary output is incomplete or inconsistent, not just a single distro family
This commit is contained in:
2026-03-26 21:28:27 -04:00
parent 3431c40af7
commit 4b3ed8cccd

View File

@@ -265,6 +265,14 @@ def extract_check_xml_timestamp_from_file(xml_path: Path) -> Optional[datetime]:
def parse_duration_seconds(raw: str) -> Optional[float]:
raw = " ".join(raw.split())
if re.fullmatch(r"\d+:\d+(?::\d+)?", raw):
parts = [int(part) for part in raw.split(":")]
if len(parts) == 2:
minutes, seconds = parts
return minutes * 60 + seconds
if len(parts) == 3:
hours, minutes, seconds = parts
return hours * 3600 + minutes * 60 + seconds
match = re.search(r"(?:(\d+)h\s+)?(?:(\d+)m\s+)?(\d+(?:\.\d+)?)(?:s)?", raw)
if not match:
return None
@@ -274,6 +282,33 @@ def parse_duration_seconds(raw: str) -> Optional[float]:
return hours * 3600 + minutes * 60 + seconds
def extract_host_results_from_run_finished_segment(segment_text: str, inventory: Dict[str, str]) -> Dict[str, HostResult]:
host_results: Dict[str, HostResult] = {}
normalized = re.sub(r"\n\s*│\s*s\s*│", "s", segment_text)
for host_match in re.finditer(
r"\s+(atvm[^\s]+)\.ts\s+([0-9:hms.\s]+?)\s+(\d+)\s+(\d+)\s+([-\d]+)\s+([-\d]+)\s+([-\d]+)",
normalized,
re.S,
):
host = host_match.group(1)
duration_seconds = parse_duration_seconds(host_match.group(2))
tests = int(host_match.group(3))
passing = int(host_match.group(4))
failing = 0 if host_match.group(5) == "-" else int(host_match.group(5))
detail = f"{tests} tests, {failing} failures"
status = "FAIL" if failing else "PASS"
host_results[host] = HostResult(
host=host,
kernel=inventory.get(host, "unknown"),
status=status,
detail=detail,
tests=tests,
failures=failing,
duration_seconds=duration_seconds,
)
return host_results
def extract_completed_subrun_summaries(log_text: str, inventory: Dict[str, str]) -> List[Dict[str, object]]:
summaries: List[Dict[str, object]] = []
cloud_blocks = list(
@@ -283,33 +318,16 @@ def extract_completed_subrun_summaries(log_text: str, inventory: Dict[str, str])
re.S,
)
)
previous_block_end = 0
for block in cloud_blocks:
block_text = block.group(1)
currents_url = block.group(2)
normalized = re.sub(r"\n\s*│\s*s\s*│", "s", block_text)
host_results: Dict[str, HostResult] = {}
for host_match in re.finditer(
r"\s+(atvm[^\s]+)\.ts\s+([0-9hms.\s]+?)\s+(\d+)\s+(\d+)\s+([-\d]+)\s+([-\d]+)\s+([-\d]+)",
normalized,
re.S,
):
host = host_match.group(1)
duration_seconds = parse_duration_seconds(host_match.group(2))
tests = int(host_match.group(3))
passing = int(host_match.group(4))
failing = 0 if host_match.group(5) == "-" else int(host_match.group(5))
detail = f"{tests} tests, {failing} failures"
status = "FAIL" if failing else "PASS"
host_results[host] = HostResult(
host=host,
kernel=inventory.get(host, "unknown"),
status=status,
detail=detail,
tests=tests,
failures=failing,
duration_seconds=duration_seconds,
)
host_results = extract_host_results_from_run_finished_segment(block_text, inventory)
if not host_results:
prior_segment = log_text[previous_block_end:block.start()]
host_results = extract_host_results_from_run_finished_segment(prior_segment, inventory)
if not host_results:
previous_block_end = block.end()
continue
summaries.append(
{
@@ -317,6 +335,7 @@ def extract_completed_subrun_summaries(log_text: str, inventory: Dict[str, str])
"currents_url": currents_url,
}
)
previous_block_end = block.end()
return summaries