From 4b3ed8cccd2ecf71d5daa4e82656012bc0056e31 Mon Sep 17 00:00:00 2001 From: "anthony.wen" Date: Thu, 26 Mar 2026 21:28:27 -0400 Subject: [PATCH] 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 --- atvm/watcher-service/atvm_run_watcher.py | 65 +++++++++++++++--------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/atvm/watcher-service/atvm_run_watcher.py b/atvm/watcher-service/atvm_run_watcher.py index 7aa8f9f..b456b98 100644 --- a/atvm/watcher-service/atvm_run_watcher.py +++ b/atvm/watcher-service/atvm_run_watcher.py @@ -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