Skip to content

Commit

Permalink
Add ErrSkipTest
Browse files Browse the repository at this point in the history
Add skip test from fixture by return special err: ErrSkipTest
  • Loading branch information
rekby authored Mar 1, 2022
1 parent db33467 commit cb1c8e3
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ run:

linters:
enable:
- golint
- revive

issues:
exclude-rules:
Expand Down
14 changes: 11 additions & 3 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@ func (e *EnvT) Cache(params interface{}, opt *FixtureOptions, f FixtureCallbackF
wrappedF := e.fixtureCallWrapper(key, f, opt)
res, err := e.c.GetOrSet(key, wrappedF)
if err != nil {
e.t.Fatalf("failed to call fixture func: %v", err)
// return not reachable after Fatalf
return nil
if errors.Is(err, ErrSkipTest) {
e.T().SkipNow()
} else {
e.t.Fatalf("failed to call fixture func: %v", err)
}

// panic must be not reachable after SkipNow or Fatalf
panic("fixenv: must be unreachable code after err check in fixture cache")
}

return res
Expand Down Expand Up @@ -179,14 +184,17 @@ func (e *EnvT) fixtureCallWrapper(key cacheKey, f FixtureCallbackFunc, opt *Fixt
// not reachable
return nil, nil
}

defer func() {
si.AddKey(key)
}()

res, err = f()

if opt.CleanupFunc != nil {
si.t.Cleanup(opt.CleanupFunc)
}

return res, err
}
}
Expand Down
89 changes: 87 additions & 2 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type testMock struct {
format string
args []interface{}
}
skipCount int
}

func (e *EnvT) cloneWithTest(t T) *EnvT {
Expand Down Expand Up @@ -63,6 +64,21 @@ func (t *testMock) Name() string {
return t.name
}

func (t *testMock) SkipNow() {
t.m.Lock()
t.skipCount++
t.m.Unlock()

runtime.Goexit()
}

func (t *testMock) Skipped() bool {
t.m.Lock()
defer t.m.Unlock()

return t.skipCount > 0
}

func Test_Env__NewEnv(t *testing.T) {
t.Run("create_new_env", func(t *testing.T) {
at := assert.New(t)
Expand Down Expand Up @@ -194,8 +210,10 @@ func Test_Env_Cache(t *testing.T) {

e := NewEnv(tMock)
at.Len(tMock.fatals, 0)
e.Cache(nil, nil, func() (res interface{}, err error) {
return nil, errors.New("test")
at.Panics(func() {
e.Cache(nil, nil, func() (res interface{}, err error) {
return nil, errors.New("test")
})
})
at.Len(tMock.fatals, 1)
})
Expand Down Expand Up @@ -316,6 +334,73 @@ func Test_FixtureWrapper(t *testing.T) {
})
}

func Test_Env_Skip(t *testing.T) {
at := assert.New(t)
tm := &testMock{name: "mock"}
tEnv := newTestEnv(tm)

skipFixtureCallTimes := 0
skipFixture := func() int {
res := tEnv.Cache(nil, nil, func() (res interface{}, err error) {
skipFixtureCallTimes++
return nil, ErrSkipTest
})
return res.(int)
}

assertGoExit := func(callback func()) {
var wg sync.WaitGroup
wg.Add(1)

// run in separate goroutine for prevent exit current goroutine
go func() {
callbackExited := true
defer func() {
at.True(callbackExited)
panicValue := recover()

// no panic value (go exit)
at.Nil(panicValue)
wg.Done()
}()

callback()
callbackExited = false
}()

wg.Wait()
}

// skip first time - with call fixture
executionStarted := false
executionStopped := true
assertGoExit(func() {
executionStarted = true
skipFixture()

executionStopped = false
})

at.True(executionStarted)
at.True(executionStopped)
at.Equal(1, skipFixtureCallTimes)

// skip second time, without call fixture - from cache
executionStarted = false
executionStopped = true
assertGoExit(func() {
executionStarted = true
skipFixture()

executionStopped = false
})

at.True(executionStarted)
at.True(executionStopped)
at.Equal(1, skipFixtureCallTimes)

}

func Test_Env_T(t *testing.T) {
at := assert.New(t)
e := NewEnv(t)
Expand Down
28 changes: 28 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fixenv

import "errors"

// Env - fixture cache engine.
type Env interface {
// T - return t object of current test/benchmark.
Expand All @@ -11,6 +13,16 @@ type Env interface {
Cache(params interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{}
}

var (
// ErrSkipTest - error for return from fixtures
// return the error mean skip test and cache decision about skip test for feature fixtures call
// as usual result/error cache.
//
// Use special error instead of detect of test.SkipNow() need for prevent run fixture in separate goroutine for
// skip detecting
ErrSkipTest = errors.New("skip test")
)

// CacheScope define life time of fixture value
// and allow use independent fixture values for different scopes, but share same value for
// one scope, which can be more then one test
Expand Down Expand Up @@ -73,4 +85,20 @@ type T interface {
// of any nested sub-tests. If two sibling sub-tests have the same name,
// Name will append a suffix to guarantee the returned name is unique.
Name() string

// SkipNow is followed by testing.T.SkipNow().
// Don't use SkipNow() for skip test from fixture - use special error ErrSkipTest for it.
//
// SkipNow marks the test as having been skipped and stops its execution
// by calling runtime.Goexit.
// If a test fails (see Error, Errorf, Fail) and is then skipped,
// it is still considered to have failed.
// Execution will continue at the next test or benchmark. See also FailNow.
// SkipNow must be called from the goroutine running the test, not from
// other goroutines created during the test. Calling SkipNow does not stop
// those other goroutines.
SkipNow()

// Skipped reports whether the test was skipped.
Skipped() bool
}
10 changes: 10 additions & 0 deletions interface_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package fixenv

import "testing"

func TestInterfaceCompatible(t *testing.T) {
// Test compile and compatible T is subinterface of testing.TB
var tb testing.TB = t
var localT T = tb
localT.Name()
}
49 changes: 45 additions & 4 deletions maintest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,29 @@ import (
// FatalfFunction function signature of Fatalf
type FatalfFunction func(format string, args ...interface{})

// SkipNowFunction is function signature for SkipNow
type SkipNowFunction func()

// CreateMainTestEnvOpts is options for manage package env scope
type CreateMainTestEnvOpts struct {
// Fatalf equivalent of Fatalf in test.
// Must write log, then exit from goroutine.
// It may be panic.
// Fatalf called if main envinment can't continue work
Fatalf FatalfFunction

// SkipNow is equivalent of SkipNow in test
// default is panic
//
// SkipNow marks the test as having been skipped and stops its execution
// by calling runtime.Goexit.
// If a test fails (see Error, Errorf, Fail) and is then skipped,
// it is still considered to have failed.
// Execution will continue at the next test or benchmark. See also FailNow.
// SkipNow must be called from the goroutine running the test, not from
// other goroutines created during the test. Calling SkipNow does not stop
// those other goroutines.
SkipNow SkipNowFunction
}

// CreateMainTestEnv called from TestMain for create global environment.
Expand All @@ -31,25 +47,35 @@ func CreateMainTestEnv(opts *CreateMainTestEnvOpts) (env *EnvT, tearDown func())

// virtualTest implement T interface for global env scope
type virtualTest struct {
m sync.Mutex
fatalf FatalfFunction
m sync.Mutex
fatalf FatalfFunction
skipNow SkipNowFunction

cleanups []func()
skipped bool
}

func newVirtualTest(opts *CreateMainTestEnvOpts) *virtualTest {
if opts == nil {
opts = &CreateMainTestEnvOpts{}
}
t := &virtualTest{
fatalf: opts.Fatalf,
fatalf: opts.Fatalf,
skipNow: opts.SkipNow,
}

if opts.Fatalf == nil {
if t.fatalf == nil {
t.fatalf = func(format string, args ...interface{}) {
panic(fmt.Sprintf(format, args...))
}
}

if t.skipNow == nil {
t.skipNow = func() {
panic("fixenv: skip called for TestMain without define skip function")
}
}

return t
}

Expand All @@ -68,6 +94,21 @@ func (t *virtualTest) Name() string {
return packageScopeName
}

func (t *virtualTest) SkipNow() {
t.m.Lock()
t.skipped = true
t.m.Unlock()

t.skipNow()
}

func (t *virtualTest) Skipped() bool {
t.m.Lock()
defer t.m.Unlock()

return t.skipped
}

func (t *virtualTest) cleanup() {
for i := len(t.cleanups) - 1; i >= 0; i-- {
t.cleanups[i]()
Expand Down
34 changes: 34 additions & 0 deletions maintest_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fixenv

import (
"runtime"
"sync"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -40,4 +42,36 @@ func TestCreateMainTestEnv(t *testing.T) {
at.Equal("asd", fFormat)
at.Equal([]interface{}{1, 2, 3}, fArgs)
})

t.Run("skip_now", func(t *testing.T) {
t.Run("default", func(t *testing.T) {
at := assert.New(t)
e, cancel := CreateMainTestEnv(nil)
defer cancel()

at.Panics(func() {
e.T().SkipNow()
})
at.True(e.T().Skipped())
})
t.Run("opt", func(t *testing.T) {
at := assert.New(t)
skipCalled := 0
e, cancel := CreateMainTestEnv(&CreateMainTestEnvOpts{SkipNow: func() {
skipCalled++
runtime.Goexit()
}})
defer cancel()

var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
e.T().SkipNow()
}()
wg.Wait()
at.Equal(1, skipCalled)
at.True(e.T().Skipped())
})
})
}

0 comments on commit cb1c8e3

Please sign in to comment.