Skip to content

testing: T.FailNow() in cleanup function causes test report to not be printed #76447

Description

@adombeck

Go version

go version go1.25.3 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/user/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/user/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build148784901=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/tmp/test/go.mod'
GOMODCACHE='/home/user/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/user/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.3.linux-amd64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='on'
GOTELEMETRYDIR='/home/user/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.3.linux-amd64/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25.3'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Calling T.FailNow() in a function that is deferred via T.Cleanup causes the test report to not be printed if there are parallel subtests and the tests are run without -test.v.

Minimal reproducer:

package main

import (
    "testing"
)

func Test(t *testing.T) {
    t.Parallel()
    t.Cleanup(func() { t.Fatal("This error is never printed") })
    t.Run("subtest", func(t *testing.T) { t.Parallel() })
}

If a subtest fails, those test failures are also not reported:

package main

import (
	"testing"
)

func Test(t *testing.T) {
	t.Parallel()
	t.Cleanup(func() { t.Fatal("This error is never printed") })
	t.Run("subtest", func(t *testing.T) {
		t.Parallel()
		t.Error("This error is also never printed")
	})
}

What did you see happen?

The test fails without any error message:

$ go test ./...
FAIL
FAIL	example.com/m	0.003s
FAIL

What did you expect to see?

I expected the test to print the test report, as it does when there are no parallel subtests:

--- FAIL: Test (0.00s)
    main_test.go:11: 
        	Error Trace:	/tmp/test/main_test.go:11
        	            				/home/user/go/src/testing/testing.go:1308
        	            				/home/user/go/src/testing/testing.go:1572
        	            				/home/user/go/src/testing/testing.go:1912
        	Error:      	This error is never printed
        	Test:       	Test
FAIL
FAIL	example.com/m	0.003s
FAIL

Activity

  1. adombeck commented on Nov 24, 2025

    @adombeck
    Author

    It's this call to t.runCleanup which never returns if one of the registered cleanup functions calls runtime.Goexit (which is called by T.FailNow).

    Printing the report in a separate deferred goroutine fixes the issue:

    diff --git a/src/testing/testing.go b/src/testing/testing.go
    index 0d1d08ca89..42840ecfea 100644
    --- a/src/testing/testing.go
    +++ b/src/testing/testing.go
    @@ -1873,6 +1873,13 @@ func tRunner(t *T, fn func(t *T)) {
     	// returned normally or because a test failure triggered
     	// a call to runtime.Goexit, record the duration and send
     	// a signal saying that the test is done.
    +	defer func() {
    +		// Output buffered logs.
    +		for root := &t.common; root.parent != nil; root = root.parent {
    +			root.flushPartial()
    +		}
    +		t.report() // Report after all subtests have finished.
    +	}()
     	defer func() {
     		t.checkRaces()
     
    @@ -2005,11 +2012,6 @@ func tRunner(t *T, fn func(t *T)) {
     			// test. See comment in Run method.
     			t.tstate.release()
     		}
    -		// Output buffered logs.
    -		for root := &t.common; root.parent != nil; root = root.parent {
    -			root.flushPartial()
    -		}
    -		t.report() // Report after all subtests have finished.
     
     		// Do not lock t.done to allow race detector to detect race in case
     		// the user does not appropriately synchronize a goroutine.

    A proper fix would also run the remaining cleanup functions and the other code that's expected to be executed afterwards in the same goroutine.

  2. added
    BugReportIssues describing a possible bug in the Go implementation.
    on Nov 24, 2025
  3. adombeck commented on Nov 25, 2025

    @adombeck
    Author

    A proper fix would also run the remaining cleanup functions and the other code that's expected to be executed afterwards in the same goroutine.

    This seems to work:

    diff --git a/src/testing/testing.go b/src/testing/testing.go
    index 0d1d08ca89..5d69aa3418 100644
    --- a/src/testing/testing.go
    +++ b/src/testing/testing.go
    @@ -1664,7 +1664,12 @@ func (c *common) runCleanup(ph panicHandling) (panicVal any) {
     		if cleanup == nil {
     			return nil
     		}
    -		cleanup()
    +		done := make(chan struct{})
    +		go func() {
    +			defer close(done) // will run even if the function calls runtime.Goexit
    +			cleanup()
    +		}()
    +		<-done
     	}
     }
  4. added this to the Go1.26 milestone on Nov 25, 2025
  5. added
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Nov 26, 2025
  6. cherrymui commented on Nov 26, 2025

    @cherrymui
    Member
  7. adombeck commented on Dec 8, 2025

    @adombeck
    Author

    @seankhliao Are you sure that this is a duplicate of #29207? I thought so too at first, but after looking at it closer I found that it's triggered by different conditions and the result is also different.

    #29207 is reproduced by:

    func TestPanic(t *testing.T) {
    	defer func() {
    		t.FailNow()
    	}()
    	panic("This panic is not printed")
    }

    The result is that the panic is not printed. This issue on the other hand is about the test report (the list of the test and subtests and whether they passed or failed) not being printed. Replacing the panic from above with a t.Error does not reproduce this issue:

    func TestError(t *testing.T) {
    	defer func() {
    		t.FailNow()
    	}()
    	t.Error("This error is printed")
    }

    Results in the report being printed:

    --- FAIL: TestError (0.00s)
        main_test.go:32: This error is printed
    

    I can only reproduce the issue with the conditions I mentioned in the description:

    Calling T.FailNow() in a function that is deferred via T.Cleanup causes the test report to not be printed if there are parallel subtests and the tests are run without -test.v.

    So these conditions are different from #29207:

    1. T.FailNow() must be called in a T.Cleanup. Using defer does not trigger the issue.
    2. There must be parallel subtests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions