From 02fb9076080b5180e8a9049be4a0c19e3dc96929 Mon Sep 17 00:00:00 2001 From: Nocccer Date: Sun, 7 Dec 2025 12:59:23 +0100 Subject: [PATCH 1/4] Add SyncSuite and split existing suite code into reusable parts. --- suite/method.go | 55 +++++++++++++++++ suite/recovery.go | 20 +++++++ suite/sharedsuite.go | 70 ++++++++++++++++++++++ suite/suite.go | 136 +++---------------------------------------- suite/synctest.go | 106 +++++++++++++++++++++++++++++++++ suite/tests.go | 21 +++++++ 6 files changed, 280 insertions(+), 128 deletions(-) create mode 100644 suite/method.go create mode 100644 suite/recovery.go create mode 100644 suite/sharedsuite.go create mode 100644 suite/synctest.go create mode 100644 suite/tests.go diff --git a/suite/method.go b/suite/method.go new file mode 100644 index 000000000..4c47d9f69 --- /dev/null +++ b/suite/method.go @@ -0,0 +1,55 @@ +package suite + +import ( + "flag" + "fmt" + "os" + "reflect" + "regexp" + "strings" + "testing" +) + +var ( + matchMethod = flag.String("testify.m", "", "regular expression to select tests of the testify suite to run") + matchMethodRE *regexp.Regexp +) + +func isTestMethod(method reflect.Method) bool { + if !strings.HasPrefix(method.Name, "Test") { + return false + } + + // compile once if needed + if *matchMethod != "" && matchMethodRE == nil { + var err error + matchMethodRE, err = regexp.Compile(*matchMethod) + if err != nil { + fmt.Fprintf(os.Stderr, "testify: invalid regexp for -m: %s\n", err) + os.Exit(1) + } + } + + // Apply -testify.m filter + if matchMethodRE != nil && !matchMethodRE.MatchString(method.Name) { + return false + } + + return true +} + +func checkMethodSignature(method reflect.Method) (test, bool) { + if method.Type.NumIn() > 1 || method.Type.NumOut() > 0 { + return test{ + name: method.Name, + run: func(t *testing.T) { + t.Errorf( + "testify: suite method %q has invalid signature: expected no input or output parameters, method has %d input parameters and %d output parameters", + method.Name, method.Type.NumIn()-1, method.Type.NumOut(), + ) + }, + }, false + } + + return test{}, true +} diff --git a/suite/recovery.go b/suite/recovery.go new file mode 100644 index 000000000..01c9c4ef5 --- /dev/null +++ b/suite/recovery.go @@ -0,0 +1,20 @@ +package suite + +import ( + "runtime/debug" + "testing" +) + +func recoverAndFailOnPanic(t *testing.T) { + t.Helper() + r := recover() + failOnPanic(t, r) +} + +func failOnPanic(t *testing.T, r interface{}) { + t.Helper() + if r != nil { + t.Errorf("test panicked: %v\n%s", r, debug.Stack()) + t.FailNow() + } +} diff --git a/suite/sharedsuite.go b/suite/sharedsuite.go new file mode 100644 index 000000000..a9d5d8fcf --- /dev/null +++ b/suite/sharedsuite.go @@ -0,0 +1,70 @@ +package suite + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type sharedSuite struct { + *assert.Assertions + + mu sync.RWMutex + require *require.Assertions + t *testing.T + + // Parent suite to have access to the implemented methods of parent struct + s TestingSuite +} + +// T retrieves the current *testing.T context. +func (suite *sharedSuite) T() *testing.T { + suite.mu.RLock() + defer suite.mu.RUnlock() + return suite.t +} + +// SetT sets the current *testing.T context. +func (suite *sharedSuite) SetT(t *testing.T) { + suite.mu.Lock() + defer suite.mu.Unlock() + suite.t = t + suite.Assertions = assert.New(t) + suite.require = require.New(t) +} + +// SetS needs to set the current test suite as parent +// to get access to the parent methods +func (suite *sharedSuite) SetS(s TestingSuite) { + suite.s = s +} + +// Require returns a require context for suite. +func (suite *sharedSuite) Require() *require.Assertions { + suite.mu.Lock() + defer suite.mu.Unlock() + if suite.require == nil { + panic("'Require' must not be called before 'Run' or 'SetT'") + } + return suite.require +} + +// Assert returns an assert context for suite. Normally, you can call: +// +// suite.NoError(err) +// +// But for situations where the embedded methods are overridden (for example, +// you might want to override assert.Assertions with require.Assertions), this +// method is provided so you can call: +// +// suite.Assert().NoError(err) +func (suite *sharedSuite) Assert() *assert.Assertions { + suite.mu.Lock() + defer suite.mu.Unlock() + if suite.Assertions == nil { + panic("'Assert' must not be called before 'Run' or 'SetT'") + } + return suite.Assertions +} diff --git a/suite/suite.go b/suite/suite.go index 32976ac9f..e513653cf 100644 --- a/suite/suite.go +++ b/suite/suite.go @@ -1,98 +1,15 @@ package suite import ( - "flag" - "fmt" - "os" "reflect" - "regexp" - "runtime/debug" - "strings" - "sync" "testing" "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -var matchMethod = flag.String("testify.m", "", "regular expression to select tests of the testify suite to run") - // Suite is a basic testing suite with methods for storing and // retrieving the current *testing.T context. type Suite struct { - *assert.Assertions - - mu sync.RWMutex - require *require.Assertions - t *testing.T - - // Parent suite to have access to the implemented methods of parent struct - s TestingSuite -} - -// T retrieves the current *testing.T context. -func (suite *Suite) T() *testing.T { - suite.mu.RLock() - defer suite.mu.RUnlock() - return suite.t -} - -// SetT sets the current *testing.T context. -func (suite *Suite) SetT(t *testing.T) { - suite.mu.Lock() - defer suite.mu.Unlock() - suite.t = t - suite.Assertions = assert.New(t) - suite.require = require.New(t) -} - -// SetS needs to set the current test suite as parent -// to get access to the parent methods -func (suite *Suite) SetS(s TestingSuite) { - suite.s = s -} - -// Require returns a require context for suite. -func (suite *Suite) Require() *require.Assertions { - suite.mu.Lock() - defer suite.mu.Unlock() - if suite.require == nil { - panic("'Require' must not be called before 'Run' or 'SetT'") - } - return suite.require -} - -// Assert returns an assert context for suite. Normally, you can call: -// -// suite.NoError(err) -// -// But for situations where the embedded methods are overridden (for example, -// you might want to override assert.Assertions with require.Assertions), this -// method is provided so you can call: -// -// suite.Assert().NoError(err) -func (suite *Suite) Assert() *assert.Assertions { - suite.mu.Lock() - defer suite.mu.Unlock() - if suite.Assertions == nil { - panic("'Assert' must not be called before 'Run' or 'SetT'") - } - return suite.Assertions -} - -func recoverAndFailOnPanic(t *testing.T) { - t.Helper() - r := recover() - failOnPanic(t, r) -} - -func failOnPanic(t *testing.T, r interface{}) { - t.Helper() - if r != nil { - t.Errorf("test panicked: %v\n%s", r, debug.Stack()) - t.FailNow() - } + sharedSuite } // Run provides suite functionality around golang subtests. It should be @@ -120,11 +37,6 @@ func (suite *Suite) Run(name string, subtest func()) bool { }) } -type test = struct { - name string - run func(t *testing.T) -} - // Run takes a testing suite and runs all of the tests attached // to it. func Run(t *testing.T, suite TestingSuite) { @@ -138,43 +50,22 @@ func Run(t *testing.T, suite TestingSuite) { stats = newSuiteInformation() } - var tests []test + var tests tests methodFinder := reflect.TypeOf(suite) suiteName := methodFinder.Elem().Name() - var matchMethodRE *regexp.Regexp - if *matchMethod != "" { - var err error - matchMethodRE, err = regexp.Compile(*matchMethod) - if err != nil { - fmt.Fprintf(os.Stderr, "testify: invalid regexp for -m: %s\n", err) - os.Exit(1) - } - } - for i := 0; i < methodFinder.NumMethod(); i++ { method := methodFinder.Method(i) - if !strings.HasPrefix(method.Name, "Test") { - continue - } - // Apply -testify.m filter - if matchMethodRE != nil && !matchMethodRE.MatchString(method.Name) { + if !isTestMethod(method) { continue } - // Check method signature - if method.Type.NumIn() > 1 || method.Type.NumOut() > 0 { - tests = append(tests, test{ - name: method.Name, - run: func(t *testing.T) { - t.Errorf( - "testify: suite method %q has invalid signature: expected no input or output parameters, method has %d input parameters and %d output parameters", - method.Name, method.Type.NumIn()-1, method.Type.NumOut(), - ) - }, - }) + + if test, ok := checkMethodSignature(method); !ok { + tests = append(tests, test) continue } + test := test{ name: method.Name, run: func(t *testing.T) { @@ -238,16 +129,5 @@ func Run(t *testing.T, suite TestingSuite) { } }() - runTests(t, tests) -} - -func runTests(t *testing.T, tests []test) { - if len(tests) == 0 { - t.Log("warning: no tests to run") - return - } - - for _, test := range tests { - t.Run(test.name, test.run) - } + tests.run(t) } diff --git a/suite/synctest.go b/suite/synctest.go new file mode 100644 index 000000000..d36173ed9 --- /dev/null +++ b/suite/synctest.go @@ -0,0 +1,106 @@ +//go:build go1.25 + +package suite + +import ( + "reflect" + "testing" + "testing/synctest" + "time" +) + +// SyncSuite is a basic testing/synctest suite with methods for storing and +// retrieving the current *testing.T context. Each method is run inside a +// synctest bubble. +type SyncSuite struct { + sharedSuite +} + +// RunSync takes a testing/synctest suite and runs all of the tests attached +// to it. Each test is run inside its own synctest bubble. +func RunSync(t *testing.T, suite TestingSuite) { + defer recoverAndFailOnPanic(t) + + suite.SetT(t) + suite.SetS(suite) + + var stats *SuiteInformation + if _, ok := suite.(WithStats); ok { + stats = newSuiteInformation() + } + + var tests tests + methodFinder := reflect.TypeOf(suite) + suiteName := methodFinder.Elem().Name() + + for i := 0; i < methodFinder.NumMethod(); i++ { + method := methodFinder.Method(i) + + if !isTestMethod(method) { + continue + } + + if test, ok := checkMethodSignature(method); !ok { + tests = append(tests, test) + continue + } + + test := test{ + name: method.Name, + run: func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + parentT := suite.T() + suite.SetT(t) + defer recoverAndFailOnPanic(t) + defer func() { + t.Helper() + + r := recover() + + stats.end(method.Name, !t.Failed() && r == nil) + + if afterTestSuite, ok := suite.(AfterTest); ok { + afterTestSuite.AfterTest(suiteName, method.Name) + } + + if tearDownTestSuite, ok := suite.(TearDownTestSuite); ok { + tearDownTestSuite.TearDownTest() + } + + suite.SetT(parentT) + failOnPanic(t, r) + }() + + if setupTestSuite, ok := suite.(SetupTestSuite); ok { + setupTestSuite.SetupTest() + } + if beforeTestSuite, ok := suite.(BeforeTest); ok { + beforeTestSuite.BeforeTest(methodFinder.Elem().Name(), method.Name) + } + + stats.start(method.Name) + + method.Func.Call([]reflect.Value{reflect.ValueOf(suite)}) + }) + }, + } + tests = append(tests, test) + } + + if len(tests) == 0 { + return + } + + if stats != nil { + stats.Start = time.Now() + } + + defer func() { + if suiteWithStats, measureStats := suite.(WithStats); measureStats { + stats.End = time.Now() + suiteWithStats.HandleStats(suiteName, stats) + } + }() + + tests.run(t) +} diff --git a/suite/tests.go b/suite/tests.go new file mode 100644 index 000000000..cc6f718da --- /dev/null +++ b/suite/tests.go @@ -0,0 +1,21 @@ +package suite + +import "testing" + +type test = struct { + name string + run func(t *testing.T) +} + +type tests []test + +func (ts tests) run(t *testing.T) { + if len(ts) == 0 { + t.Log("warning: no tests to run") + return + } + + for _, test := range ts { + t.Run(test.name, test.run) + } +} From ca73009688211a8b17aef786f74ea5b80da20ac7 Mon Sep 17 00:00:00 2001 From: Nocccer Date: Sun, 7 Dec 2025 13:03:38 +0100 Subject: [PATCH 2/4] Update workflow --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2cd4ad32b..a69562ed3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,6 +33,7 @@ jobs: - "1.21" - "1.22" - "1.23" + - "1.25" steps: - uses: actions/checkout@v5 - name: Setup Go From ae4f2c5ba40d22e0fc5cff75216acfbba649b578 Mon Sep 17 00:00:00 2001 From: Nocccer Date: Sun, 18 Jan 2026 15:07:22 +0100 Subject: [PATCH 3/4] Add tests for synctest suite --- suite/suite_test.go | 6 +- suite/{synctest.go => syncsuite.go} | 7 + suite/syncsuite_test.go | 551 ++++++++++++++++++++++++++++ 3 files changed, 562 insertions(+), 2 deletions(-) rename suite/{synctest.go => syncsuite.go} (89%) create mode 100644 suite/syncsuite_test.go diff --git a/suite/suite_test.go b/suite/suite_test.go index 1c193aaf2..52edfac1a 100644 --- a/suite/suite_test.go +++ b/suite/suite_test.go @@ -377,7 +377,6 @@ func TestRunSuite(t *testing.T) { // called Skip() assert.Equal(t, 1, suiteSkipTester.SetupSuiteRunCount) assert.Equal(t, 1, suiteSkipTester.TearDownSuiteRunCount) - } // This suite has no Test... methods. It's setup and teardown must be skipped. @@ -393,7 +392,6 @@ func (s *SuiteSetupSkipTester) SetupSuite() { } func (s *SuiteSetupSkipTester) NonTestMethod() { - } func (s *SuiteSetupSkipTester) TearDownSuite() { @@ -491,6 +489,7 @@ func (s *CallOrderSuite) call(method string) { func TestSuiteCallOrder(t *testing.T) { Run(t, new(CallOrderSuite)) } + func (s *CallOrderSuite) SetupSuite() { s.call("SetupSuite") } @@ -499,6 +498,7 @@ func (s *CallOrderSuite) TearDownSuite() { s.call("TearDownSuite") assert.Equal(s.T(), "SetupSuite;SetupTest;Test A;SetupSubTest;SubTest A1;TearDownSubTest;SetupSubTest;SubTest A2;TearDownSubTest;TearDownTest;SetupTest;Test B;SetupSubTest;SubTest B1;TearDownSubTest;SetupSubTest;SubTest B2;TearDownSubTest;TearDownTest;TearDownSuite", strings.Join(s.callOrder, ";")) } + func (s *CallOrderSuite) SetupTest() { s.call("SetupTest") } @@ -660,6 +660,7 @@ func TestFailfastSuiteFailFastOn(t *testing.T) { t.Fail() } } + func (s *FailfastSuite) SetupSuite() { s.call("SetupSuite") } @@ -667,6 +668,7 @@ func (s *FailfastSuite) SetupSuite() { func (s *FailfastSuite) TearDownSuite() { s.call("TearDownSuite") } + func (s *FailfastSuite) SetupTest() { s.call("SetupTest") } diff --git a/suite/synctest.go b/suite/syncsuite.go similarity index 89% rename from suite/synctest.go rename to suite/syncsuite.go index d36173ed9..b2fac6122 100644 --- a/suite/synctest.go +++ b/suite/syncsuite.go @@ -16,6 +16,13 @@ type SyncSuite struct { sharedSuite } +// SetTime is a helper function to set the fake clock of the synctest +// bubble to the given time. Only timestamps after 1.Jan 2000 is allowed, +// because synctest time starts at 1. Jan 2000 +func (s *SyncSuite) SetTime(ts time.Time) { + time.Sleep(time.Until(ts)) +} + // RunSync takes a testing/synctest suite and runs all of the tests attached // to it. Each test is run inside its own synctest bubble. func RunSync(t *testing.T, suite TestingSuite) { diff --git a/suite/syncsuite_test.go b/suite/syncsuite_test.go new file mode 100644 index 000000000..7c7106f16 --- /dev/null +++ b/suite/syncsuite_test.go @@ -0,0 +1,551 @@ +//go:build go1.25 + +package suite + +import ( + "bytes" + "flag" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// SuiteRequireTwice is intended to test the usage of suite.Require in two +// different tests +type SyncSuiteRequireTwice struct{ SyncSuite } + +// TestSuiteRequireTwice checks for regressions of issue #149 where +// suite.requirements was not initialized in suite.SetT() +// A regression would result on these tests panicking rather than failing. +func TestSyncSuiteRequireTwice(t *testing.T) { + ok := testing.RunTests( + allTestsFilter, + []testing.InternalTest{{ + Name: t.Name() + "/SyncSuiteRequireTwice", + F: func(t *testing.T) { + suite := new(SuiteRequireTwice) + Run(t, suite) + }, + }}, + ) + assert.False(t, ok) +} + +func (s *SyncSuiteRequireTwice) TestRequireOne() { + r := s.Require() + r.Equal(1, 2) +} + +func (s *SyncSuiteRequireTwice) TestRequireTwo() { + r := s.Require() + r.Equal(1, 2) +} + +type panickingSyncSuite struct { + SyncSuite + panicInSetupTest bool + panicInBeforeTest bool + panicInTest bool + panicInAfterTest bool + panicInTearDownTest bool +} + +func (s *panickingSyncSuite) SetupTest() { + if s.panicInSetupTest { + panic("oops in setup test") + } +} + +func (s *panickingSyncSuite) BeforeTest(_, _ string) { + if s.panicInBeforeTest { + panic("oops in before test") + } +} + +func (s *panickingSyncSuite) Test() { + if s.panicInTest { + panic("oops in test") + } +} + +func (s *panickingSyncSuite) AfterTest(_, _ string) { + if s.panicInAfterTest { + panic("oops in after test") + } +} + +func (s *panickingSyncSuite) TearDownTest() { + if s.panicInTearDownTest { + panic("oops in tear down test") + } +} + +func TestSyncSuiteRecoverPanic(t *testing.T) { + ok := true + panickingTests := []testing.InternalTest{ + { + Name: t.Name() + "/InSetupTest", + F: func(t *testing.T) { Run(t, &panickingSyncSuite{panicInSetupTest: true}) }, + }, + { + Name: t.Name() + "InBeforeTest", + F: func(t *testing.T) { Run(t, &panickingSyncSuite{panicInBeforeTest: true}) }, + }, + { + Name: t.Name() + "/InTest", + F: func(t *testing.T) { Run(t, &panickingSyncSuite{panicInTest: true}) }, + }, + { + Name: t.Name() + "/InAfterTest", + F: func(t *testing.T) { Run(t, &panickingSyncSuite{panicInAfterTest: true}) }, + }, + { + Name: t.Name() + "/InTearDownTest", + F: func(t *testing.T) { Run(t, &panickingSyncSuite{panicInTearDownTest: true}) }, + }, + } + + require.NotPanics(t, func() { + ok = testing.RunTests(allTestsFilter, panickingTests) + }) + + assert.False(t, ok) +} + +// This suite is intended to store values to make sure that only +// testing-suite-related methods are run. It's also a fully +// functional example of a testing suite, using setup/teardown methods +// and a helper method that is ignored by testify. To make this look +// more like a real world example, all tests in the suite perform some +// type of assertion. +type SyncSuiteTester struct { + // Include our basic suite logic. + SyncSuite + + // Keep counts of how many times each method is run. + SetupTestRunCount int + TearDownTestRunCount int + TestOneRunCount int + TestTwoRunCount int + NonTestMethodRunCount int + + SuiteNameBefore []string + TestNameBefore []string + + SuiteNameAfter []string + TestNameAfter []string + + TimeBefore []time.Time + TimeAfter []time.Time +} + +func (suite *SyncSuiteTester) BeforeTest(suiteName, testName string) { + suite.SuiteNameBefore = append(suite.SuiteNameBefore, suiteName) + suite.TestNameBefore = append(suite.TestNameBefore, testName) + suite.TimeBefore = append(suite.TimeBefore, time.Now()) +} + +func (suite *SyncSuiteTester) AfterTest(suiteName, testName string) { + suite.SuiteNameAfter = append(suite.SuiteNameAfter, suiteName) + suite.TestNameAfter = append(suite.TestNameAfter, testName) + suite.TimeAfter = append(suite.TimeAfter, time.Now()) +} + +// The SetupTest method will be run before every test in the suite. +func (suite *SyncSuiteTester) SetupTest() { + suite.SetupTestRunCount++ +} + +// The TearDownTest method will be run after every test in the suite. +func (suite *SyncSuiteTester) TearDownTest() { + suite.TearDownTestRunCount++ +} + +// Every method in a testing suite that begins with "Test" will be run +// as a test. TestOne is an example of a test. For the purposes of +// this example, we've included assertions in the tests, since most +// tests will issue assertions. +func (suite *SyncSuiteTester) TestOne() { + beforeCount := suite.TestOneRunCount + suite.TestOneRunCount++ + assert.Equal(suite.T(), suite.TestOneRunCount, beforeCount+1) + suite.Equal(suite.TestOneRunCount, beforeCount+1) +} + +// TestTwo is another example of a test. +func (suite *SyncSuiteTester) TestTwo() { + beforeCount := suite.TestTwoRunCount + suite.TestTwoRunCount++ + assert.NotEqual(suite.T(), suite.TestTwoRunCount, beforeCount) + suite.NotEqual(suite.TestTwoRunCount, beforeCount) +} + +func (suite *SyncSuiteTester) TestSkip() { + suite.T().Skip() +} + +// NonTestMethod does not begin with "Test", so it will not be run by +// testify as a test in the suite. This is useful for creating helper +// methods for your tests. +func (suite *SyncSuiteTester) NonTestMethod() { + suite.NonTestMethodRunCount++ +} + +// TestRunSuite will be run by the 'go test' command, so within it, we +// can run our suite using the Run(*testing.T, TestingSuite) function. +func TestRunSyncSuite(t *testing.T) { + suiteTester := new(SyncSuiteTester) + Run(t, suiteTester) + + // Normally, the test would end here. The following are simply + // some assertions to ensure that the Run function is working as + // intended - they are not part of the example. + + assert.Len(t, suiteTester.SuiteNameAfter, 3) + assert.Len(t, suiteTester.SuiteNameBefore, 3) + assert.Len(t, suiteTester.TestNameAfter, 3) + assert.Len(t, suiteTester.TestNameBefore, 3) + + assert.Contains(t, suiteTester.TestNameAfter, "TestOne") + assert.Contains(t, suiteTester.TestNameAfter, "TestTwo") + assert.Contains(t, suiteTester.TestNameAfter, "TestSkip") + + assert.Contains(t, suiteTester.TestNameBefore, "TestOne") + assert.Contains(t, suiteTester.TestNameBefore, "TestTwo") + assert.Contains(t, suiteTester.TestNameBefore, "TestSkip") + + for _, suiteName := range suiteTester.SuiteNameAfter { + assert.Equal(t, "SyncSuiteTester", suiteName) + } + + for _, suiteName := range suiteTester.SuiteNameBefore { + assert.Equal(t, "SyncSuiteTester", suiteName) + } + + for _, when := range suiteTester.TimeAfter { + assert.False(t, when.IsZero()) + } + + for _, when := range suiteTester.TimeBefore { + assert.False(t, when.IsZero()) + } + + // There are three test methods (TestOne, TestTwo and TestSkip), so + // the SetupTest and TearDownTest methods (which should be run once for + // each test) should have been run three times. + assert.Equal(t, 3, suiteTester.SetupTestRunCount) + assert.Equal(t, 3, suiteTester.TearDownTestRunCount) + + // Each test should have been run once. + assert.Equal(t, 1, suiteTester.TestOneRunCount) + assert.Equal(t, 1, suiteTester.TestTwoRunCount) + + // Methods that don't match the test method identifier shouldn't + // have been run at all. + assert.Equal(t, 0, suiteTester.NonTestMethodRunCount) +} + +// This suite has no Test... methods. It's setup and teardown must be skipped. +type SyncSuiteSetupSkipTester struct { + SyncSuite + + setUp bool + toreDown bool +} + +func (s *SyncSuiteSetupSkipTester) SetupSuite() { + s.setUp = true +} + +func (s *SyncSuiteSetupSkipTester) NonTestMethod() { +} + +func (s *SyncSuiteSetupSkipTester) TearDownSuite() { + s.toreDown = true +} + +func TestSkippingSyncSuiteSetup(t *testing.T) { + suiteTester := new(SyncSuiteSetupSkipTester) + Run(t, suiteTester) + assert.False(t, suiteTester.setUp) + assert.False(t, suiteTester.toreDown) +} + +func TestSyncSuiteGetters(t *testing.T) { + suite := new(SyncSuiteTester) + suite.SetT(t) + assert.NotNil(t, suite.Assert()) + assert.Equal(t, suite.Assertions, suite.Assert()) + assert.NotNil(t, suite.Require()) + assert.Equal(t, suite.require, suite.Require()) +} + +type SyncSuiteLoggingTester struct { + SyncSuite +} + +func (s *SyncSuiteLoggingTester) TestLoggingPass() { + s.T().Log("TESTLOGPASS") +} + +func (s *SyncSuiteLoggingTester) TestLoggingFail() { + s.T().Log("TESTLOGFAIL") + assert.NotNil(s.T(), nil) // expected to fail +} + +func TestSyncSuiteLogging(t *testing.T) { + suiteLoggingTester := new(SyncSuiteLoggingTester) + capture := StdoutCapture{} + internalTest := testing.InternalTest{ + Name: t.Name() + "/SyncSuiteLoggingTester", + F: func(subT *testing.T) { + Run(subT, suiteLoggingTester) + }, + } + capture.StartCapture() + testing.RunTests(allTestsFilter, []testing.InternalTest{internalTest}) + output, err := capture.StopCapture() + require.NoError(t, err, "Got an error trying to capture stdout and stderr!") + require.NotEmpty(t, output, "output content must not be empty") + + // Failed tests' output is always printed + assert.Contains(t, output, "TESTLOGFAIL") + + if testing.Verbose() { + // In verbose mode, output from successful tests is also printed + assert.Contains(t, output, "TESTLOGPASS") + } else { + assert.NotContains(t, output, "TESTLOGPASS") + } +} + +type syncSuiteWithStats struct { + SyncSuite + wasCalled bool + stats *SuiteInformation +} + +func (s *syncSuiteWithStats) HandleStats(suiteName string, stats *SuiteInformation) { + s.wasCalled = true + s.stats = stats +} + +func (s *syncSuiteWithStats) TestSomething() { + s.Equal(1, 1) +} + +func (s *syncSuiteWithStats) TestPanic() { + panic("oops") +} + +func TestSyncSuiteWithStats(t *testing.T) { + syncSuiteWithStats := new(syncSuiteWithStats) + + suiteSuccess := testing.RunTests(allTestsFilter, []testing.InternalTest{ + { + Name: t.Name() + "/syncSuiteWithStats", + F: func(t *testing.T) { + Run(t, syncSuiteWithStats) + }, + }, + }) + require.False(t, suiteSuccess, "syncSuiteWithStats should report test failure because of panic in TestPanic") + + assert.True(t, syncSuiteWithStats.wasCalled) + assert.NotZero(t, syncSuiteWithStats.stats.Start) + assert.NotZero(t, syncSuiteWithStats.stats.End) + assert.False(t, syncSuiteWithStats.stats.Passed()) + + testStats := syncSuiteWithStats.stats.TestStats + + assert.NotZero(t, testStats["TestSomething"].Start) + assert.NotZero(t, testStats["TestSomething"].End) + assert.True(t, testStats["TestSomething"].Passed) + + assert.NotZero(t, testStats["TestPanic"].Start) + assert.NotZero(t, testStats["TestPanic"].End) + assert.False(t, testStats["TestPanic"].Passed) +} + +// FailfastSyncSuite will test the behavior when running with the failfast flag +// It logs calls in the callOrder slice which we then use to assert the correct calls were made +type FailfastSyncSuite struct { + SyncSuite + callOrder []string +} + +func (s *FailfastSyncSuite) call(method string) { + s.callOrder = append(s.callOrder, method) +} + +func TestFailfastSyncSuite(t *testing.T) { + // This test suite is run twice. Once normally and once with the -failfast flag by TestFailfastSuiteFailFastOn + // If you need to debug it run this test directly with the failfast flag set on/off as you need + failFast := flag.Lookup("test.failfast").Value.(flag.Getter).Get().(bool) + s := new(FailfastSyncSuite) + ok := testing.RunTests( + allTestsFilter, + []testing.InternalTest{{ + Name: t.Name() + "/FailfastSyncSuite", + F: func(t *testing.T) { + Run(t, s) + }, + }}, + ) + assert.False(t, ok) + var expect []string + if failFast { + expect = []string{"SetupTest", "Test A Fails", "TearDownTest"} + } else { + expect = []string{"SetupTest", "Test A Fails", "TearDownTest", "SetupTest", "Test B Passes", "TearDownTest"} + } + callOrderAssert(t, expect, s.callOrder) +} + +func TestFailfastSyncSuiteFailFastOn(t *testing.T) { + // To test this with failfast on (and isolated from other intended test failures in our test suite) we launch it in its own process + cmd := exec.Command("go", "test", "-v", "-race", "-run", "TestFailfastSyncSuite", "-failfast") + var out bytes.Buffer + cmd.Stdout = &out + t.Log("Running go test -v -race -run TestFailfastSyncSuite -failfast") + err := cmd.Run() + t.Log(out.String()) + if err != nil { + t.Log(err) + t.Fail() + } +} + +func (s *FailfastSyncSuite) SetupTest() { + s.call("SetupTest") +} + +func (s *FailfastSyncSuite) TearDownTest() { + s.call("TearDownTest") +} + +func (s *FailfastSyncSuite) Test_A_Fails() { + s.call("Test A Fails") + s.T().Error("Test A meant to fail") +} + +func (s *FailfastSyncSuite) Test_B_Passes() { + s.call("Test B Passes") + s.Require().True(true) +} + +type unInitializedSyncSuite struct { + SyncSuite +} + +// TestUnInitializedSuites asserts the behavior of the suite methods when the +// suite is not initialized +func TestUnInitializedSyncSuites(t *testing.T) { + t.Run("should panic on Require", func(t *testing.T) { + suite := new(unInitializedSyncSuite) + + assert.Panics(t, func() { + suite.Require().True(true) + }) + }) + + t.Run("should panic on Assert", func(t *testing.T) { + suite := new(unInitializedSyncSuite) + + assert.Panics(t, func() { + suite.Assert().True(true) + }) + }) +} + +// SyncSuiteSignatureValidationTester tests valid and invalid method signatures. +type SyncSuiteSignatureValidationTester struct { + SyncSuite + + executedTestCount int +} + +// Valid test method — should run. +func (s *SyncSuiteSignatureValidationTester) TestValidSignature() { + s.executedTestCount++ +} + +// Invalid: has return value. +func (s *SyncSuiteSignatureValidationTester) TestInvalidSignatureReturnValue() interface{} { + s.executedTestCount++ + return nil +} + +// Invalid: has input arg. +func (s *SyncSuiteSignatureValidationTester) TestInvalidSignatureArg(somearg string) { + s.executedTestCount++ +} + +// Invalid: both input arg and return value. +func (s *SyncSuiteSignatureValidationTester) TestInvalidSignatureBoth(somearg string) interface{} { + s.executedTestCount++ + return nil +} + +// TestSuiteSignatureValidation ensures that invalid signature methods fail and valid method runs. +func TestSyncSuiteSignatureValidation(t *testing.T) { + suiteTester := new(SyncSuiteSignatureValidationTester) + + ok := testing.RunTests(allTestsFilter, []testing.InternalTest{ + { + Name: "signature validation", + F: func(t *testing.T) { + Run(t, suiteTester) + }, + }, + }) + + require.False(t, ok, "Suite should fail due to invalid method signatures") + + assert.Equal(t, 1, suiteTester.executedTestCount, "Only the valid test method should have been executed") +} + +type syncSuiteTimeTest struct { + SyncSuite + ticker *time.Ticker +} + +func (s *syncSuiteTimeTest) SetupTest() { + // there will no test that have a timeout set to 24h + s.ticker = time.NewTicker(time.Hour * 24) +} + +func (s *syncSuiteTimeTest) TestTimeIsAdvanced() { + // check if ticker is advanced and the test does not timeout + <-s.ticker.C +} + +// Check if suite test is called in a synctest bubble and aswell if the +// ticker is created inside the bubble (SetupTest called inside synctest.Test). +func TestSyncSuiteTimeTest(t *testing.T) { + t.Setenv("GODEBUG", "asynctimerchan=0") // since our go.mod says `go 1.17` + RunSync(t, new(syncSuiteTimeTest)) +} + +type syncSuiteSetTime struct { + SyncSuite +} + +func (s *syncSuiteSetTime) TestSetTime() { + // time is not set because timestamp is before 1. Jan 2000 + s.SetTime(time.Date(1970, time.January, 1, 1, 0, 1, 0, time.Local)) + s.Equal(time.Date(2000, time.January, 1, 1, 0, 0, 0, time.Local), time.Now()) + + // time is advanced to given timestamp + ts := time.Date(2001, time.January, 1, 1, 0, 1, 0, time.Local) + s.SetTime(ts) + s.Equal(ts, time.Now()) +} + +func TestSyncSuiteSetTime(t *testing.T) { + t.Setenv("GODEBUG", "asynctimerchan=0") // since our go.mod says `go 1.17` + RunSync(t, new(syncSuiteSetTime)) +} From c914bfb5fa5ac299551c66733f2323e87dbfeaea Mon Sep 17 00:00:00 2001 From: Nocccer Date: Sun, 18 Jan 2026 15:33:01 +0100 Subject: [PATCH 4/4] Change test time to UTC --- suite/syncsuite.go | 5 +++++ suite/syncsuite_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/suite/syncsuite.go b/suite/syncsuite.go index b2fac6122..f506dfa4f 100644 --- a/suite/syncsuite.go +++ b/suite/syncsuite.go @@ -23,6 +23,11 @@ func (s *SyncSuite) SetTime(ts time.Time) { time.Sleep(time.Until(ts)) } +// Wait is just a wrapper for synctest.Wait() +func (s *SyncSuite) Wait() { + synctest.Wait() +} + // RunSync takes a testing/synctest suite and runs all of the tests attached // to it. Each test is run inside its own synctest bubble. func RunSync(t *testing.T, suite TestingSuite) { diff --git a/suite/syncsuite_test.go b/suite/syncsuite_test.go index 7c7106f16..d88aa2638 100644 --- a/suite/syncsuite_test.go +++ b/suite/syncsuite_test.go @@ -536,13 +536,13 @@ type syncSuiteSetTime struct { func (s *syncSuiteSetTime) TestSetTime() { // time is not set because timestamp is before 1. Jan 2000 - s.SetTime(time.Date(1970, time.January, 1, 1, 0, 1, 0, time.Local)) - s.Equal(time.Date(2000, time.January, 1, 1, 0, 0, 0, time.Local), time.Now()) + s.SetTime(time.Date(1970, time.January, 1, 1, 0, 1, 0, time.UTC)) + s.Equal(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), time.Now().UTC()) // time is advanced to given timestamp - ts := time.Date(2001, time.January, 1, 1, 0, 1, 0, time.Local) + ts := time.Date(2001, time.January, 1, 1, 0, 0, 0, time.UTC) s.SetTime(ts) - s.Equal(ts, time.Now()) + s.Equal(ts, time.Now().UTC()) } func TestSyncSuiteSetTime(t *testing.T) {