Skip to content

Commit 4fc15ef

Browse files
authored
Merge pull request #1220 from ousamabenyounes/fix/issue-958
fix(go): prevent double-counted failures when test-level fail also triggers package-level fail (#958)
2 parents aaf4d1d + 6634712 commit 4fc15ef

1 file changed

Lines changed: 96 additions & 3 deletions

File tree

‎src/cmds/go/go_cmd.rs‎

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,13 @@ fn filter_go_test_json(output: &str) -> String {
391391
let total_fail: usize = packages.values().map(|p| p.fail).sum();
392392
let total_skip: usize = packages.values().map(|p| p.skip).sum();
393393
let total_build_fail: usize = packages.values().filter(|p| p.build_failed).count();
394-
let total_pkg_fail: usize = packages.values().filter(|p| p.package_failed).count();
394+
// Only count package-level fails for packages with no individual test or build failures.
395+
// go test -json emits a trailing package-level {"action":"fail"} after any test failure
396+
// too, but that event is just a cascade — the individual test failures are already counted.
397+
let total_pkg_fail: usize = packages
398+
.values()
399+
.filter(|p| p.package_failed && p.fail == 0 && !p.build_failed)
400+
.count();
395401

396402
let has_failures = total_fail > 0 || total_build_fail > 0 || total_pkg_fail > 0;
397403

@@ -418,9 +424,11 @@ fn filter_go_test_json(output: &str) -> String {
418424
result.push_str(&format!(" in {} packages\n", total_packages));
419425
result.push_str("═══════════════════════════════════════\n");
420426

421-
// Show package-level failures first (timeouts, signals, panics)
427+
// Show package-level failures first (timeouts, signals, panics).
428+
// Skip packages that already have individual test-level failures — those are displayed
429+
// in the per-package section below and the package-level event is just a cascade.
422430
for (package, pkg_result) in packages.iter() {
423-
if !pkg_result.package_failed {
431+
if !pkg_result.package_failed || pkg_result.fail > 0 || pkg_result.build_failed {
424432
continue;
425433
}
426434

@@ -754,6 +762,91 @@ mod tests {
754762
);
755763
}
756764

765+
#[test]
766+
fn test_filter_go_test_no_double_count_on_test_failure() {
767+
// go test -json always emits a package-level {"action":"fail"} after each
768+
// test-level failure. The package-level event is a cascade, not an additional
769+
// failure. The summary header must show "1 failed", not "2 failed".
770+
let output = r#"{"Time":"2024-01-01T10:00:00Z","Action":"run","Package":"example.com/foo","Test":"TestFail"}
771+
{"Time":"2024-01-01T10:00:01Z","Action":"output","Package":"example.com/foo","Test":"TestFail","Output":"=== RUN TestFail\n"}
772+
{"Time":"2024-01-01T10:00:02Z","Action":"output","Package":"example.com/foo","Test":"TestFail","Output":" Error: expected 5, got 3\n"}
773+
{"Time":"2024-01-01T10:00:03Z","Action":"fail","Package":"example.com/foo","Test":"TestFail","Elapsed":0.5}
774+
{"Time":"2024-01-01T10:00:03Z","Action":"fail","Package":"example.com/foo","Elapsed":0.5}"#;
775+
776+
let result = filter_go_test_json(output);
777+
// The summary header must say "1 failed", not "2 failed" (no double-counting).
778+
assert!(
779+
result.starts_with("Go test: 0 passed, 1 failed"),
780+
"Expected header 'Go test: 0 passed, 1 failed', got: {}",
781+
result
782+
);
783+
assert!(result.contains("TestFail"));
784+
assert!(result.contains("expected 5, got 3"));
785+
// The package must NOT appear twice (once as "[FAIL]" and once with test details).
786+
assert_eq!(
787+
result.matches("foo").count(),
788+
1,
789+
"Package name should appear exactly once, got: {}",
790+
result
791+
);
792+
}
793+
794+
#[test]
795+
fn test_filter_go_test_timeout_with_signal_quit_output() {
796+
// Exact reproduction of the scenario from issue #958: the signal: quit line
797+
// appears as a separate JSON output event.
798+
let output = r#"{"Action":"start","Package":"example.com/pkg"}
799+
{"Action":"output","Package":"example.com/pkg","Output":"*** Test killed with quit: ran too long (1m30s).\n"}
800+
{"Action":"output","Package":"example.com/pkg","Output":"signal: quit\n"}
801+
{"Action":"output","Package":"example.com/pkg","Output":"FAIL\texample.com/pkg\t90.000s\n"}
802+
{"Action":"fail","Package":"example.com/pkg","Elapsed":90.001}"#;
803+
804+
let result = filter_go_test_json(output);
805+
assert!(
806+
result.starts_with("Go test: 0 passed, 1 failed"),
807+
"Expected 'Go test: 0 passed, 1 failed', got: {}",
808+
result
809+
);
810+
assert!(
811+
!result.contains("No tests found"),
812+
"Must not say 'No tests found' on timeout, got: {}",
813+
result
814+
);
815+
assert!(
816+
result.contains("Test killed with quit"),
817+
"Should show the timeout message, got: {}",
818+
result
819+
);
820+
}
821+
822+
#[test]
823+
fn test_filter_go_test_timeout_with_passing_tests_before_kill() {
824+
// Some tests pass before the package times out.
825+
// Summary should show both pass and fail counts.
826+
let output = r#"{"Action":"run","Package":"example.com/foo","Test":"TestFast"}
827+
{"Action":"pass","Package":"example.com/foo","Test":"TestFast","Elapsed":0.001}
828+
{"Action":"run","Package":"example.com/foo","Test":"TestHang"}
829+
{"Action":"output","Package":"example.com/foo","Output":"*** Test killed with quit: ran too long (30s).\n"}
830+
{"Action":"fail","Package":"example.com/foo","Elapsed":30.001}"#;
831+
832+
let result = filter_go_test_json(output);
833+
assert!(
834+
result.starts_with("Go test: 1 passed, 1 failed"),
835+
"Expected 'Go test: 1 passed, 1 failed', got: {}",
836+
result
837+
);
838+
assert!(
839+
!result.contains("No tests found"),
840+
"Must not say 'No tests found', got: {}",
841+
result
842+
);
843+
assert!(
844+
result.contains("Test killed with quit"),
845+
"Should show timeout message, got: {}",
846+
result
847+
);
848+
}
849+
757850
#[test]
758851
fn test_filter_go_build_success() {
759852
let output = "";

0 commit comments

Comments
 (0)