testing: record a test's failure for -failfast after cleanup runs

The -failfast flag stops a test binary from starting new tests after
the first failure.

When a test had a parallel subtest, its failure was recorded before
its cleanup functions ran. A failure inside a t.Cleanup was therefore
not counted, and -failfast kept starting new tests. Tests without a
parallel subtest run their cleanup earlier and were unaffected.

Record the failure after cleanup has run, so a cleanup failure counts
for -failfast regardless of whether the test has a parallel subtest.

Fixes #61034

Change-Id: Icea654cbaf3904936200566d7d7d72ca8ccc5958
GitHub-Last-Rev: e46ad5ad314c5cee2a835be3627f4a6455b81c32
GitHub-Pull-Request: golang/go#80165
Reviewed-on: https://go-review.googlesource.com/c/go/+/794541
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/src/cmd/go/testdata/script/test_fail_fast.txt b/src/cmd/go/testdata/script/test_fail_fast.txt
index 1f169d6..89a3049 100644
--- a/src/cmd/go/testdata/script/test_fail_fast.txt
+++ b/src/cmd/go/testdata/script/test_fail_fast.txt
@@ -32,6 +32,13 @@
 ! go test ./failfast_test.go -run='TestParallelFailingSubtestsA' -failfast=true
 stdout -count=1 'FAIL - '
 
+# a failure in a cleanup of a test with a parallel subtest must stop failfast
+# (go.dev/issue/61034)
+! go test ./failfast_test.go -run='TestCleanupFailingWithParallelSubtestA|TestFailingB' -failfast=true
+stdout -count=1 'FAIL - '
+! go test ./failfast_test.go -run='TestCleanupFailingWithParallelSubtestA|TestFailingB' -failfast=false
+stdout -count=2 'FAIL - '
+
 # only parallels
 ! go test ./failfast_test.go -run='TestParallelFailing[AB]' -failfast=false
 stdout -count=2 'FAIL - '
@@ -109,6 +116,18 @@
 	})
 }
 
+func TestCleanupFailingWithParallelSubtestA(t *testing.T) {
+	// Regression test for go.dev/issue/61034: a failure in a Cleanup
+	// function must be counted for -failfast even when the test has a
+	// parallel subtest.
+	t.Cleanup(func() {
+		t.Errorf("FAIL - %s", t.Name())
+	})
+	t.Run("sub", func(t *testing.T) {
+		t.Parallel()
+	})
+}
+
 func TestFailingB(t *testing.T) {
 	t.Errorf("FAIL - %s", t.Name())
 }
diff --git a/src/testing/testing.go b/src/testing/testing.go
index 8b7742d..832d9e5 100644
--- a/src/testing/testing.go
+++ b/src/testing/testing.go
@@ -2037,11 +2037,6 @@
 	defer func() {
 		t.checkRaces()
 
-		// TODO(#61034): This is the wrong place for this check.
-		if t.Failed() {
-			numFailed.Add(1)
-		}
-
 		// Check if the test panicked or Goexited inappropriately.
 		//
 		// If this happens in a normal test, print output but continue panicking.
@@ -2170,6 +2165,14 @@
 		for root := &t.common; root.parent != nil; root = root.parent {
 			root.flushPartial()
 		}
+
+		// Record this test's failure for -failfast. This must happen after the
+		// test's cleanup functions have run, since a cleanup may call t.Fail.
+		// See go.dev/issue/61034.
+		if t.Failed() {
+			numFailed.Add(1)
+		}
+
 		t.report() // Report after all subtests have finished.
 
 		// Do not lock t.done to allow race detector to detect race in case