diff --git a/context/src/junit/parser.rs b/context/src/junit/parser.rs index e0e0c2b5..6eb93e79 100644 --- a/context/src/junit/parser.rs +++ b/context/src/junit/parser.rs @@ -782,6 +782,15 @@ impl JunitParser { if !matches!(test_case.status, TestCaseStatus::Success { .. }) { return; // Only set status once } + // Playwright's junit reporter (with includeRetries) emits the / + // attempts BEFORE the closing /. Those reruns were + // parsed while the status was still Success, so quick-junit filed them under + // flaky_runs; move them onto the terminal status so a test that fails every retry + // keeps its earlier attempts instead of collapsing to a single run. + let prior_reruns = match &mut test_case.status { + TestCaseStatus::Success { flaky_runs } => mem::take(flaky_runs), + _ => Vec::new(), + }; let tag = e.name(); let mut test_case_status = if tag.as_ref() == TAG_TEST_CASE_STATUS_SKIPPED { TestCaseStatus::skipped() @@ -802,6 +811,7 @@ impl JunitParser { test_case_status.set_type(r#type); } + test_case_status.add_reruns(prior_reruns); test_case.status = test_case_status; } else { self.issues.push(JunitParseIssue::Invalid( @@ -2122,4 +2132,80 @@ failures: ] ); } + + #[test] + #[allow(deprecated)] + fn test_into_test_case_runs_emits_run_per_failing_rerun_playwright_order() { + // Faithful to Playwright's junit reporter (includeRetries) for a test that failed every + // attempt: the / attempts and the testcase are + // emitted BEFORE the closing terminal element, the reruns are a MIX of rerunFailure + // (assertion) and rerunError (timeout), and the terminal element is (attempt 0 + // timed out), not . Regression test for those attempts being dropped when the + // trailing terminal element replaced the accumulated status. + let mut junit_parser = JunitParser::new(); + let file_contents = r#" + + + + + + + + + + + + + + + + + + page.waitForTimeout: Test timeout of 10000ms exceeded. + + + + "#; + junit_parser + .parse(BufReader::new(file_contents.as_bytes())) + .unwrap(); + + let test_case_runs = junit_parser.into_test_case_runs(IntoTestCaseRunsOptions { + org_slug: "org", + repo: &RepoUrlParts { + host: "host".into(), + owner: "owner".into(), + name: "name".into(), + }, + codeowners: None, + quarantined_test_ids: &[], + variant: "", + test_runner_config: None, + }); + + // All four attempts (3 reruns + the terminal error) surface as their own runs; the proto + // has no dedicated error status, so every attempt is recorded as a failure. + assert_eq!(test_case_runs.len(), 4); + assert!( + test_case_runs + .iter() + .all(|run| run.status == TestCaseRunStatus::Failure as i32) + ); + // Each attempt's own message survives — including the rerunError and the two rerunFailure + // assertions that were previously discarded. + let messages: Vec<&str> = test_case_runs + .iter() + .filter_map(|run| run.test_output.as_ref()) + .map(|out| out.message.as_str()) + .collect(); + assert!( + messages + .contains(&"/^Pacific\\b/ hour 11 from \"11:00:03 A.M.\" should be even") + ); + assert!( + messages + .contains(&"/^Pacific\\b/ hour 11 from \"11:00:23 A.M.\" should be even") + ); + assert!(messages.contains(&"Test timeout of 10000ms exceeded.")); + } }