Include failure detail in ATVM host status output

This commit is contained in:
2026-03-30 16:07:20 -04:00
parent 944a13ec26
commit 6000fa0e78

View File

@@ -349,7 +349,7 @@ def parse_host_xml(xml_path: Path) -> Optional[Tuple[str, HostResult]]:
return None
root = tree.getroot()
suites = root.findall("testsuite")
best: Optional[Tuple[str, int, int, float, Optional[datetime]]] = None
best: Optional[Tuple[str, int, int, float, Optional[datetime], Optional[str]]] = None
for suite in suites:
file_attr = suite.attrib.get("file", "")
suite_name = suite.attrib.get("name", "")
@@ -367,17 +367,20 @@ def parse_host_xml(xml_path: Path) -> Optional[Tuple[str, HostResult]]:
failures = int(float(suite.attrib.get("failures", root.attrib.get("failures", "0"))))
total_time = float(suite.attrib.get("time", root.attrib.get("time", "0")))
timestamp = parse_xml_timestamp(suite.attrib.get("timestamp"))
candidate = (host_name, tests, failures, total_time, timestamp)
failure_detail = extract_failure_detail_from_xml_suite(suite)
candidate = (host_name, tests, failures, total_time, timestamp, failure_detail)
if best is None:
best = candidate
continue
_, best_tests, _, best_total_time, _ = best
_, best_tests, _, best_total_time, _, _ = best
if tests > best_tests or (tests == best_tests and total_time >= best_total_time):
best = candidate
if not best:
return None
file_name, tests, failures, total_time, timestamp = best
file_name, tests, failures, total_time, timestamp, failure_detail = best
detail = f"{tests} tests, {failures} failures"
if failures:
detail = append_failure_detail(detail, failure_detail)
status = "FAIL" if failures else "PASS"
return file_name, HostResult(
host=file_name,
@@ -423,6 +426,66 @@ def parse_duration_seconds(raw: str) -> Optional[float]:
return hours * 3600 + minutes * 60 + seconds
def compact_failure_detail(raw: str, limit: int = 220) -> str:
normalized = " ".join(raw.split())
if len(normalized) <= limit:
return normalized
return normalized[: limit - 3].rstrip() + "..."
def append_failure_detail(detail: str, failure_detail: Optional[str]) -> str:
if not failure_detail or failure_detail in detail:
return detail
return f"{detail} - {failure_detail}"
def extract_failure_detail_from_xml_suite(suite: ET.Element) -> Optional[str]:
for testcase in suite.findall("testcase"):
failure = testcase.find("failure")
if failure is None:
continue
case_name = testcase.attrib.get("classname") or testcase.attrib.get("name") or "failed testcase"
parts = []
message = failure.attrib.get("message")
if message:
parts.append(message)
if failure.text:
parts.append(failure.text)
failure_text = compact_failure_detail(" ".join(parts)) if parts else ""
if failure_text:
return compact_failure_detail(f"{case_name}: {failure_text}")
return compact_failure_detail(case_name)
return None
def extract_failure_detail_from_text_blob(text: str) -> Optional[str]:
for pattern in (
r"(Error:\s.*?)(?:\n\s*\n|$)",
r"(AssertionError:\s.*?)(?:\n\s*\n|$)",
r"(Timed out!.*?)(?:\n\s*\n|$)",
):
match = re.search(pattern, text, re.I | re.S)
if match:
return compact_failure_detail(match.group(1))
return None
def extract_failure_detail_from_log_text(log_text: str, host: str) -> Optional[str]:
pattern = (
rf"\d+\)\s+Testing .*?{re.escape(host)}.*?\n"
rf"\s+([^\n]+):\n"
rf"\s+(.*?)(?:\n\s*\n|\n\s*<?xml|\n\s*Screenshots:|\n\s*Video output:|$)"
)
match = re.search(pattern, log_text, re.S)
if not match:
return None
testcase = match.group(1).strip()
body = compact_failure_detail(match.group(2))
if body:
return compact_failure_detail(f"{testcase} - {body}")
return compact_failure_detail(testcase)
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)
@@ -463,13 +526,17 @@ def extract_completed_subrun_summaries(log_text: str, inventory: Dict[str, str])
for block in cloud_blocks:
block_text = block.group(1)
currents_url = block.group(2)
prior_segment = log_text[previous_block_end:block.start()]
detail_source = prior_segment + "\n" + block_text
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
for host, result in host_results.items():
if result.failures:
result.detail = append_failure_detail(result.detail, extract_failure_detail_from_log_text(detail_source, host))
summaries.append(
{
"host_results": host_results,
@@ -631,6 +698,23 @@ def parse_host_reporter_json(artifact_path: Path, host: str, kernels: Dict[str,
if pending:
detail_parts.append(f"{pending} pending")
failure_detail = None
tests_payload = payload.get("tests")
if failures and isinstance(tests_payload, dict):
for testcase_name, testcase_events in tests_payload.items():
if not isinstance(testcase_name, str) or not isinstance(testcase_events, list):
continue
for event in testcase_events:
if not isinstance(event, list) or len(event) < 3:
continue
status_value = str(event[2]).lower()
message_value = str(event[1]) if len(event) > 1 else ""
if status_value in {"failed", "fail", "error"} or re.search(r"\b(Error|AssertionError|Timed out!)\b", message_value):
failure_detail = compact_failure_detail(f"{testcase_name} - {message_value}")
break
if failure_detail:
break
if failures:
status = "FAIL"
elif pending:
@@ -638,11 +722,15 @@ def parse_host_reporter_json(artifact_path: Path, host: str, kernels: Dict[str,
else:
status = "PASS"
detail = ", ".join(detail_parts)
if failures:
detail = append_failure_detail(detail, failure_detail)
return HostResult(
host=host,
kernel=kernels.get(host, "unknown"),
status=status,
detail=", ".join(detail_parts),
detail=detail,
tests=tests,
failures=failures,
duration_seconds=float(duration_ms) / 1000 if duration_ms else None,
@@ -666,6 +754,8 @@ def parse_host_reporter_artifact(artifact_path: Path, host: str, kernels: Dict[s
failures = 1 if re.search(r"\bTimed out!\b|\bAssertionError\b|\bFAIL\b|\berror\b", text, re.I) else 0
status = "FAIL" if failures else "PASS"
detail = "1 failures" if failures else "completed"
if failures:
detail = append_failure_detail(detail, extract_failure_detail_from_text_blob(text))
return HostResult(
host=host,
kernel=kernels.get(host, "unknown"),