diff --git a/atvm/docs/automation/run-learnings.md b/atvm/docs/automation/run-learnings.md index 37de4e4..532a135 100644 --- a/atvm/docs/automation/run-learnings.md +++ b/atvm/docs/automation/run-learnings.md @@ -553,3 +553,13 @@ This file stores run-specific examples only when a run produced a new learning r - Action for future runs: - Do not stop parent summary parsing at the Recorded Run detection log line. - Bound each `Cloud Run Finished` block by the next run boundary such as the next `Extracted specPattern:` or the next `Cloud Run Finished`, then parse all host rows inside that block. + +## Run Learning: 2026-04-16 (Categorized `Cloud Run Finished` parsing must stop at the Recorded Run URL for each grouped batch) +- Observed failure mode: + - A categorized ATVM run completed its Windows batch in the Cypress launch log, but the watcher posted only the earlier grouped results and never sent a separate Windows Mattermost status. + - The watcher let one categorized `Cloud Run Finished` block run forward into the next grouped batch because the next grouped run did not present a fresh `Extracted specPattern:` boundary before the next runner output. + - That let host-row parsing drift across grouped runs, which caused the Windows batch XML to be relabeled under the wrong subrun and left the real Windows subrun stuck in `RUNNING`. +- Action for future runs: + - For categorized grouped recovery, stop each `Cloud Run Finished` block at that grouped run's `🏁 Recorded Run:` line when it is present. + - Do not let categorized summary parsing continue into the next grouped batch's runner output. + - Keep grouped host-row parsing scoped to the actual summary table rows for that grouped run only. diff --git a/atvm/watcher-service/atvm_run_watcher.py b/atvm/watcher-service/atvm_run_watcher.py index bd2344b..bee2c04 100644 --- a/atvm/watcher-service/atvm_run_watcher.py +++ b/atvm/watcher-service/atvm_run_watcher.py @@ -673,11 +673,10 @@ def summarize_host_detail_with_mochawesome(detail: str, testcase: str, message: 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) + 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]+)", + r"(?m)^\s*│\s*([✔✖])\s+(atvm[^\s]+)\.ts\s+([0-9:hms. ]+?)\s+(\d+)\s+(\d+)\s+([-\d]+)\s+([-\d]+)\s+([-\d]+)\s*│\s*$", normalized, - re.S, ): host = host_match.group(2) duration_seconds = parse_duration_seconds(host_match.group(3)) @@ -700,12 +699,18 @@ def extract_host_results_from_run_finished_segment(segment_text: str, inventory: def extract_completed_subrun_summaries(log_text: str, inventory: Dict[str, str]) -> List[Dict[str, object]]: summaries: List[Dict[str, object]] = [] - cloud_starts = [match.start() for match in re.finditer(r"Cloud Run Finished", log_text)] + cloud_starts = [match.start() for match in re.finditer(r"(?m)^\s*Cloud Run Finished\s*$", log_text)] previous_block_end = 0 for index, block_start in enumerate(cloud_starts): next_cloud_start = cloud_starts[index + 1] if index + 1 < len(cloud_starts) else len(log_text) + section_text = log_text[block_start:next_cloud_start] + recorded_run_match = re.search(r"🏁 Recorded Run:\s*(https://\S+)", section_text) next_spec_match = re.search(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} - INFO - Extracted specPattern:", log_text[block_start + 1 :], re.M) block_end = next_cloud_start + if recorded_run_match: + candidate_end = block_start + recorded_run_match.end() + if candidate_end < block_end: + block_end = candidate_end if next_spec_match: candidate_end = block_start + 1 + next_spec_match.start() if candidate_end < block_end: @@ -1826,7 +1831,7 @@ def discover_categorized_subruns( if summary and (not host_results or all(result.host == "check-xml-files" for result in host_results.values())): host_results = summary["host_results"] completed_hosts.extend([host for host in host_results if host not in completed_hosts]) - if not host_results and check_ts: + if check_ts: group_host_results = collect_group_host_reporter_artifacts( reporter_root=reporter_root, group_label=raw_display_group, @@ -1835,7 +1840,9 @@ def discover_categorized_subruns( run_ended_at=check_ts + timedelta(seconds=5), ) if group_host_results: - host_results = group_host_results + merged_results = dict(host_results) + merged_results.update(group_host_results) + host_results = merged_results completed_hosts.extend([host for host in host_results if host not in completed_hosts]) if not host_results and check_ts: