Skip to content

Commit

Permalink
feat: improve handling of repeated parallel test setup (#61)
Browse files Browse the repository at this point in the history
Signed-off-by: tkrop <[email protected]>
  • Loading branch information
tkrop committed Dec 16, 2023
1 parent 3f3c666 commit b4ea734
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 24 deletions.
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ endif
# request targets, while the single target can be used to define the
# precondition of custom target.
.PHONY: $(TARGETS) $(addprefix target/,$(TARGETS))
$(TARGETS):; $(GOBIN)/go-make $(MAKEFLAGS) $(MAKECMDGOALS);
$(eval $(lastwords $(MAKECMDGOALS)):;@:)
$(firstword $(MAKECMDGOALS)):
$(GOBIN)/go-make $(MAKEFLAGS) $(MAKECMDGOALS);
$(addprefix target/,$(TARGETS)): target/%:
$(GOBIN)/go-make $(MAKEFLAGS) $*;

Expand Down
5 changes: 5 additions & 0 deletions internal/mock/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ var (
Params: []*Param{},
Results: []*Param{{Type: "string"}},
Variadic: false,
}, {
Name: "Parallel",
Params: []*Param{},
Results: []*Param{},
Variadic: false,
}, {
Name: "TempDir",
Params: []*Param{},
Expand Down
2 changes: 1 addition & 1 deletion test/caller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ var (
}()
// CallerTestErrorf provides the file with the line number of the `Errorf`
// call in testing.
CallerTestErrorf = path.Join(SourceDir, "testing.go:180")
CallerTestErrorf = path.Join(SourceDir, "testing.go:204")
// CallerGomockErrorf provides the file with the line number of the
// `Errorf` call in gomock.
CallerGomockErrorf = path.Join(SourceDir, "gomock.go:61")
Expand Down
74 changes: 52 additions & 22 deletions test/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ const (
Parallel = true
)

// ensureParallel ensures that the test runs test parameter sets in parallel.
func ensureParallel(t *testing.T) {
t.Helper()
defer func() {
if v := recover(); v != nil &&
v != "testing: t.Parallel called multiple times" {
panic(v)
}
}()
t.Parallel()
}

// TODO: consider following convenience methods:
//
// // Result is a convenience method that returns the first argument ans swollows
Expand Down Expand Up @@ -87,11 +99,22 @@ type Reporter interface {
// Test is a minimal interface for abstracting test methods that are needed to
// setup an isolated test environment for GoMock and Testify.
type Test interface {
Helper()
// Name provides the test name.
Name() string
// Helper declares a test helper function.
Helper()
// Parallel declares that the test is to be run in parallel with (and only
// with) other parallel tests.
Parallel()
// TempDir creates a new temporary directory for the test.
TempDir() string
// Errorf handles a failure messages when a test is supposed to continue.
Errorf(format string, args ...any)
// Fatalf handles a fatal failure messge that immediate aborts of the test
// execution.
Fatalf(format string, args ...any)
// FailNow handles fatal failure notifications without log output that
// aborts test execution immediately.
FailNow()
}

Expand Down Expand Up @@ -124,13 +147,6 @@ func NewTester(t Test, expect Expect) *Tester {
return (&Tester{t: t, expect: expect})
}

// Parallel delegates request to `testing.T.Parallel()`.
func (t *Tester) Parallel() {
if t, ok := t.t.(*testing.T); ok {
t.Parallel()
}
}

// WaitGroup adds wait group to unlock in case of a failure.
//
//revive:disable-next-line:waitgroup-by-value // own wrapper interface
Expand Down Expand Up @@ -158,19 +174,27 @@ func (t *Tester) Name() string {
return t.t.Name()
}

// TempDir delegates the request to the parent test context.
func (t *Tester) TempDir() string {
return t.t.TempDir()
}

// Helper delegates request to the parent test context.
func (t *Tester) Helper() {
t.t.Helper()
}

// Parallel delegates request to the parent context if it is of type
// `*testing.T`. Else it is swallowing the request silently.
func (t *Tester) Parallel() {
if t, ok := t.t.(*testing.T); ok {
ensureParallel(t)
}
}

// TempDir delegates the request to the parent test context.
func (t *Tester) TempDir() string {
return t.t.TempDir()
}

// Errorf handles failure messages where the test is supposed to continue. On
// an expected success, the failure is also delegated to the parent test
// context.
// context. Else it delegates the request to the test reporter if available.
func (t *Tester) Errorf(format string, args ...any) {
t.Helper()
t.failed.Store(true)
Expand All @@ -183,7 +207,8 @@ func (t *Tester) Errorf(format string, args ...any) {

// Fatalf handles a fatal failure messge that immediate aborts of the test
// execution. On an expected success, the failure handling is also delegated
// to the parent test context.
// to the parent test context. Else it delegates the request to the test
// reporter if available.
func (t *Tester) Fatalf(format string, args ...any) {
t.Helper()
t.failed.Store(true)
Expand All @@ -198,7 +223,8 @@ func (t *Tester) Fatalf(format string, args ...any) {

// FailNow handles fatal failure notifications without log output that aborts
// test execution immediately. On an expected success, it the failure handling
// is also delegated to the parent test context.
// is also delegated to the parent test context. Else it delegates the request
// to the test reporter if available.
func (t *Tester) FailNow() {
t.Helper()
t.failed.Store(true)
Expand Down Expand Up @@ -391,15 +417,21 @@ func (r *runner[P]) RunSeq(call func(t Test, param P)) Runner[P] {
return r.run(call, false)
}

// parallel ensures that the test runner runs the test parameter sets in
// parallel.
func (r *runner[P]) parallel(parallel bool) {
if parallel {
ensureParallel(r.t)
}
}

// Run runs the test parameter sets either parallel or in sequence.
func (r *runner[P]) run(
call func(t Test, param P), parallel bool,
) Runner[P] {
switch params := r.params.(type) {
case map[string]P:
if parallel {
r.t.Parallel()
}
r.parallel(parallel)
r.wg.Add(len(params))

for name, param := range params {
Expand All @@ -408,9 +440,7 @@ func (r *runner[P]) run(
}

case []P:
if parallel {
r.t.Parallel()
}
r.parallel(parallel)
r.wg.Add(len(params))

for index, param := range params {
Expand Down
19 changes: 19 additions & 0 deletions test/testing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,22 @@ func TestTypePanic(t *testing.T) {
test.New[TestParam](t, ParamParam{expect: false}).
Run(func(t test.Test, param TestParam) {})
}

func TestParallel(t *testing.T) {
t.Parallel()
test.New[ParamParam](t, []ParamParam{{expect: false}}).
Run(func(t test.Test, param ParamParam) {
t.Parallel()
})
}

func TestParallelDenied(t *testing.T) {
t.Setenv("TESTING", "true")
defer func() {
assert.Equal(t, "testing: t.Parallel called after t.Setenv;"+
" cannot set environment variables in parallel tests", recover())
}()

test.New[ParamParam](t, []ParamParam{{expect: false}}).
Run(func(t test.Test, param ParamParam) {})
}

0 comments on commit b4ea734

Please sign in to comment.