Include failure detail in ATVM host status output
This commit is contained in:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user