Skip to content

Commit

Permalink
Merge move cleanup func from options to return value
Browse files Browse the repository at this point in the history
  • Loading branch information
rekby authored Jun 12, 2022
2 parents 8b26ce5 + c86d47b commit 9994d0f
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 37 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ jobs:
run: go build -v ./...

- name: Test
run: go test ./...

- name: Test with coverage profiler
if: ${{ matrix.goVersion == env.GO_VERSION }}
run: go test -test.count=10 -race -covermode atomic -coverprofile=covprofile.out ./...

- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
Expand Down
49 changes: 42 additions & 7 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,40 @@ func (e *EnvT) T() T {
// f - callback - fixture body.
// Cache guarantee for call f exactly once for same Cache called and params combination.
func (e *EnvT) Cache(params interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{} {
return e.cache(params, opt, f)
}

// CacheWithCleanup call from fixture and manage call f and cache it.
// CacheWithCleanup must be called direct from fixture - it use runtime stacktrace for
// detect called method - it is part of cache key.
// params - part of cache key. Usually - parameters, passed to fixture.
// it allow use parametrized fixtures with different results.
// params must be json serializable.
// opt - fixture options, nil for default options.
// f - callback - fixture body.
// cleanup, returned from f called while fixture cleanup
// Cache guarantee for call f exactly once for same Cache called and params combination.
func (e *EnvT) CacheWithCleanup(params interface{}, opt *FixtureOptions, f FixtureCallbackWithCleanupFunc) interface{} {
if opt == nil {
opt = &FixtureOptions{}
}

var resCleanupFunc FixtureCleanupFunc

var fWithoutCleanup FixtureCallbackFunc = func() (res interface{}, err error) {
res, resCleanupFunc, err = f()
return res, err
}
opt.cleanupFunc = func() {
if resCleanupFunc != nil {
resCleanupFunc()
}
}

return e.cache(params, opt, fWithoutCleanup)
}

func (e *EnvT) cache(params interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{} {
if opt == nil {
opt = globalEmptyFixtureOptions
}
Expand Down Expand Up @@ -122,17 +156,18 @@ func (e *EnvT) onCreate() {
// makeCacheKey generate cache key
// must be called from first level of env functions - for detect external caller
func makeCacheKey(testname string, params interface{}, opt *FixtureOptions, testCall bool) (cacheKey, error) {
externalCallerLevel := 4
externalCallerLevel := 5
var pc = make([]uintptr, externalCallerLevel)
var extCallerFrame runtime.Frame
if externalCallerLevel == runtime.Callers(0, pc) {
frames := runtime.CallersFrames(pc)
frames.Next() // callers
frames.Next() // the function
frames.Next() // caller of the function
frames.Next() // caller of the function (env private function)
frames.Next() // caller of private function (env public function)
extCallerFrame, _ = frames.Next() // external caller
}
scopeName := scopeName(testname, opt.Scope)
scopeName := makeScopeName(testname, opt.Scope)
return makeCacheKeyFromFrame(params, opt.Scope, extCallerFrame, scopeName, testCall)
}

Expand Down Expand Up @@ -173,7 +208,7 @@ func makeCacheKeyFromFrame(params interface{}, scope CacheScope, f runtime.Frame

func (e *EnvT) fixtureCallWrapper(key cacheKey, f FixtureCallbackFunc, opt *FixtureOptions) FixtureCallbackFunc {
return func() (res interface{}, err error) {
scopeName := scopeName(e.t.Name(), opt.Scope)
scopeName := makeScopeName(e.t.Name(), opt.Scope)

e.m.Lock()
si := e.scopes[scopeName]
Expand All @@ -191,15 +226,15 @@ func (e *EnvT) fixtureCallWrapper(key cacheKey, f FixtureCallbackFunc, opt *Fixt

res, err = f()

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

return res, err
}
}

func scopeName(testName string, scope CacheScope) string {
func makeScopeName(testName string, scope CacheScope) string {
switch scope {
case ScopePackage:
return packageScopeName
Expand Down
12 changes: 12 additions & 0 deletions env_generic_sugar.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,15 @@ func Cache[TRes any](env Env, params any, opt *FixtureOptions, f func() (TRes, e
}
return res
}

func CacheWithCleanup[TRes any](env Env, params any, opt *FixtureOptions, f func() (TRes, FixtureCleanupFunc, error)) TRes {
callbackResult := env.CacheWithCleanup(params, opt, func() (res interface{}, cleanup FixtureCleanupFunc, err error) {
return f()
})

var res TRes
if callbackResult != nil {
res = callbackResult.(TRes)
}
return res
}
30 changes: 29 additions & 1 deletion env_generic_sugar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,32 @@ func TestCacheGeneric(t *testing.T) {
require.Equal(t, 2, res)
}

func TestCacheWithCleanupGeneric(t *testing.T) {
inParams := 123
inOpt := &FixtureOptions{Scope: ScopeTest}

cleanupCalledBack := 0

env := envMock{onCacheWithCleanup: func(params interface{}, opt *FixtureOptions, f FixtureCallbackWithCleanupFunc) interface{} {
require.Equal(t, inParams, params)
require.Equal(t, inOpt, opt)
res, _, _ := f()
return res
}}

res := CacheWithCleanup(env, inParams, inOpt, func() (int, FixtureCleanupFunc, error) {
cleanup := func() {
cleanupCalledBack++
}
return 2, cleanup, nil
})
require.Equal(t, 2, res)

}

type envMock struct {
onCache func(params interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{}
onCache func(params interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{}
onCacheWithCleanup func(params interface{}, opt *FixtureOptions, f FixtureCallbackWithCleanupFunc) interface{}
}

func (e envMock) T() T {
Expand All @@ -37,3 +61,7 @@ func (e envMock) T() T {
func (e envMock) Cache(params interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{} {
return e.onCache(params, opt, f)
}

func (e envMock) CacheWithCleanup(params interface{}, opt *FixtureOptions, f FixtureCallbackWithCleanupFunc) interface{} {
return e.onCacheWithCleanup(params, opt, f)
}
83 changes: 70 additions & 13 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type testMock struct {
Expand Down Expand Up @@ -278,6 +279,58 @@ func Test_Env_Cache(t *testing.T) {
})
}

func Test_Env_CacheWithCleanup(t *testing.T) {
t.Run("NilCleanup", func(t *testing.T) {
tMock := &testMock{name: t.Name()}
env := newTestEnv(tMock)

callbackCalled := 0
var callbackFunc FixtureCallbackWithCleanupFunc = func() (res interface{}, cleanup FixtureCleanupFunc, err error) {
callbackCalled++
return callbackCalled, nil, nil
}

res := env.CacheWithCleanup(nil, nil, callbackFunc)
require.Equal(t, 1, res)
require.Equal(t, 1, callbackCalled)

// got value from cache
res = env.CacheWithCleanup(nil, nil, callbackFunc)
require.Equal(t, 1, res)
require.Equal(t, 1, callbackCalled)
})

t.Run("WithCleanup", func(t *testing.T) {
tMock := &testMock{name: t.Name()}
env := newTestEnv(tMock)

callbackCalled := 0
cleanupCalled := 0
var callbackFunc FixtureCallbackWithCleanupFunc = func() (res interface{}, cleanup FixtureCleanupFunc, err error) {
callbackCalled++
cleanup = func() {
cleanupCalled++
}
return callbackCalled, cleanup, nil
}

res := env.CacheWithCleanup(nil, nil, callbackFunc)
require.Equal(t, 1, res)
require.Equal(t, 1, callbackCalled)
require.Equal(t, cleanupCalled, 0)

// got value from cache
res = env.CacheWithCleanup(nil, nil, callbackFunc)
require.Equal(t, 1, res)
require.Equal(t, 1, callbackCalled)
require.Equal(t, cleanupCalled, 0)

tMock.callCleanup()
require.Equal(t, 1, callbackCalled)
require.Equal(t, 1, cleanupCalled)
})
}

func Test_FixtureWrapper(t *testing.T) {
t.Run("ok", func(t *testing.T) {
at := assert.New(t)
Expand All @@ -293,7 +346,7 @@ func Test_FixtureWrapper(t *testing.T) {
cnt++
return cnt, errors.New("test")
}, &FixtureOptions{})
si := e.scopes[scopeName(tMock.Name(), ScopeTest)]
si := e.scopes[makeScopeName(tMock.Name(), ScopeTest)]
at.Equal(0, cnt)
at.Len(si.cacheKeys, 0)
res1, err := w()
Expand All @@ -308,7 +361,7 @@ func Test_FixtureWrapper(t *testing.T) {
w = e.fixtureCallWrapper(key2, func() (res interface{}, err error) {
cnt++
return cnt, nil
}, &FixtureOptions{CleanupFunc: func() {
}, &FixtureOptions{cleanupFunc: func() {

}})
at.Len(tMock.cleanups, cleanupsLen)
Expand Down Expand Up @@ -416,7 +469,7 @@ func Test_Env_TearDown(t *testing.T) {

e1 := newTestEnv(t1)
at.Len(e1.scopes, 1)
at.Len(e1.scopes[scopeName(t1.name, ScopeTest)].Keys(), 0)
at.Len(e1.scopes[makeScopeName(t1.name, ScopeTest)].Keys(), 0)
at.Len(e1.c.store, 0)

e1.Cache(1, nil, func() (res interface{}, err error) {
Expand All @@ -426,31 +479,31 @@ func Test_Env_TearDown(t *testing.T) {
return nil, nil
})
at.Len(e1.scopes, 1)
at.Len(e1.scopes[scopeName(t1.name, ScopeTest)].Keys(), 2)
at.Len(e1.scopes[makeScopeName(t1.name, ScopeTest)].Keys(), 2)
at.Len(e1.c.store, 2)

t2 := &testMock{name: "mock2"}
// defer t2.callCleanup - direct call e2.tearDown - for test

e2 := e1.cloneWithTest(t2)
at.Len(e1.scopes, 2)
at.Len(e1.scopes[scopeName(t1.name, ScopeTest)].Keys(), 2)
at.Len(e1.scopes[scopeName(t2.name, ScopeTest)].Keys(), 0)
at.Len(e1.scopes[makeScopeName(t1.name, ScopeTest)].Keys(), 2)
at.Len(e1.scopes[makeScopeName(t2.name, ScopeTest)].Keys(), 0)
at.Len(e1.c.store, 2)

e2.Cache(1, nil, func() (res interface{}, err error) {
return nil, nil
})

at.Len(e1.scopes, 2)
at.Len(e1.scopes[scopeName(t1.name, ScopeTest)].Keys(), 2)
at.Len(e1.scopes[scopeName(t2.name, ScopeTest)].Keys(), 1)
at.Len(e1.scopes[makeScopeName(t1.name, ScopeTest)].Keys(), 2)
at.Len(e1.scopes[makeScopeName(t2.name, ScopeTest)].Keys(), 1)
at.Len(e1.c.store, 3)

// finish first test and tearDown e1
e1.tearDown()
at.Len(e1.scopes, 1)
at.Len(e1.scopes[scopeName(t2.name, ScopeTest)].Keys(), 1)
at.Len(e1.scopes[makeScopeName(t2.name, ScopeTest)].Keys(), 1)
at.Len(e1.c.store, 1)

e2.tearDown()
Expand Down Expand Up @@ -479,10 +532,14 @@ func Test_MakeCacheKey(t *testing.T) {
var res cacheKey
var err error

envFunc := func() {
privateEnvFunc := func() {
res, err = makeCacheKey("asdf", 222, globalEmptyFixtureOptions, true)
}
envFunc()

publicEnvFunc := func() {
privateEnvFunc()
}
publicEnvFunc() // external caller
at.NoError(err)

expected := cacheKey(`{"func":"github.com/rekby/fixenv.Test_MakeCacheKey","fname":".../env_test.go","scope":0,"scope_name":"asdf","params":222}`)
Expand Down Expand Up @@ -601,15 +658,15 @@ func Test_ScopeName(t *testing.T) {
for _, c := range table {
t.Run(c.name, func(t *testing.T) {
at := assert.New(t)
at.Equal(c.result, scopeName(c.testName, c.scope))
at.Equal(c.result, makeScopeName(c.testName, c.scope))
})
}
})

t.Run("unexpected_scope", func(t *testing.T) {
at := assert.New(t)
at.Panics(func() {
scopeName("asd", -1)
makeScopeName("asd", -1)
})
})
}
8 changes: 4 additions & 4 deletions examples/custom_env/custom_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ func NewEnv(t *testing.T) (context.Context, *Env) {
}

func testServer(e fixenv.Env, response string) *httptest.Server {
return fixenv.Cache(e, response, nil, func() (_ *httptest.Server, err error) {
return fixenv.CacheWithCleanup(e, response, nil, func() (_ *httptest.Server, cleanup fixenv.FixtureCleanupFunc, err error) {
resp := []byte(response)

server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write(resp)
}))
e.T().(testing.TB).Logf("Http server start. %q url: %q", response, server.URL)
e.T().Cleanup(func() {
cleanup = func() {
server.Close()
e.T().(testing.TB).Logf("Http server stop. %q url: %q", response, server.URL)
})
return server, nil
}
return server, cleanup, nil
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ func NewEnv(t *testing.T) *Env {
}

func testServer(e *Env) *httptest.Server {
return fixenv.Cache(e, "", nil, func() (res *httptest.Server, err error) {
return fixenv.CacheWithCleanup(e, "", nil, func() (res *httptest.Server, cleanup fixenv.FixtureCleanupFunc, err error) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte(e.Resp))
}))
e.T().(testing.TB).Logf("Http server start, url: %q", server.URL)
e.T().Cleanup(func() {
cleanup = func() {
server.Close()
e.T().(testing.TB).Logf("Http server stop, url: %q", server.URL)
})
return server, nil
}
return server, cleanup, nil
})
}

Expand Down
8 changes: 4 additions & 4 deletions examples/simple/http_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ import (
)

func testServer(e fixenv.Env, response string) *httptest.Server {
return e.Cache(response, nil, func() (res interface{}, err error) {
return e.CacheWithCleanup(response, nil, func() (res interface{}, cleanup fixenv.FixtureCleanupFunc, err error) {
resp := []byte(response)

server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write(resp)
}))
e.T().(testing.TB).Logf("Http server start. %q url: %q", response, server.URL)
e.T().Cleanup(func() {
cleanup = func() {
server.Close()
e.T().(testing.TB).Logf("Http server stop. %q url: %q", response, server.URL)
})
return server, nil
}
return server, cleanup, nil
}).(*httptest.Server)
}

Expand Down
Loading

0 comments on commit 9994d0f

Please sign in to comment.