From 21b6244f6d3f3f5d177ff7414e1f20fe06ac911c Mon Sep 17 00:00:00 2001 From: rekby Date: Sun, 12 Jun 2022 15:06:11 +0300 Subject: [PATCH 1/5] add CacheWithCleanup method to env --- env.go | 52 ++++++++++-- env_generic_sugar.go | 12 +++ env_generic_sugar_test.go | 7 +- env_test.go | 83 ++++++++++++++++--- examples/custom_env/custom_env_test.go | 8 +- .../custom_env_with_shared_content_test.go | 8 +- examples/simple/http_server_test.go | 8 +- interface.go | 20 ++++- 8 files changed, 162 insertions(+), 36 deletions(-) diff --git a/env.go b/env.go index 2134276..b8727e7 100644 --- a/env.go +++ b/env.go @@ -64,6 +64,43 @@ 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{} + } + if opt.cleanupFunc != nil { + e.t.Fatalf("CacheWithCleanup not compatible with CleanupFunc param") + } + + 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 } @@ -122,17 +159,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) } @@ -173,7 +211,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] @@ -191,15 +229,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 diff --git a/env_generic_sugar.go b/env_generic_sugar.go index 99d3637..82c0110 100644 --- a/env_generic_sugar.go +++ b/env_generic_sugar.go @@ -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 +} diff --git a/env_generic_sugar_test.go b/env_generic_sugar_test.go index 41f5b9d..66d2d20 100644 --- a/env_generic_sugar_test.go +++ b/env_generic_sugar_test.go @@ -27,7 +27,8 @@ func TestCacheGeneric(t *testing.T) { } 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 { @@ -37,3 +38,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) +} diff --git a/env_test.go b/env_test.go index a56f8ff..e470144 100644 --- a/env_test.go +++ b/env_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type testMock struct { @@ -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 := NewEnv(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 := NewEnv(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) @@ -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() @@ -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) @@ -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) { @@ -426,7 +479,7 @@ 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"} @@ -434,8 +487,8 @@ func Test_Env_TearDown(t *testing.T) { 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) { @@ -443,14 +496,14 @@ func Test_Env_TearDown(t *testing.T) { }) 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() @@ -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}`) @@ -601,7 +658,7 @@ 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)) }) } }) @@ -609,7 +666,7 @@ func Test_ScopeName(t *testing.T) { t.Run("unexpected_scope", func(t *testing.T) { at := assert.New(t) at.Panics(func() { - scopeName("asd", -1) + makeScopeName("asd", -1) }) }) } diff --git a/examples/custom_env/custom_env_test.go b/examples/custom_env/custom_env_test.go index 3e02706..ca83b4a 100644 --- a/examples/custom_env/custom_env_test.go +++ b/examples/custom_env/custom_env_test.go @@ -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 }) } diff --git a/examples/custom_env_with_shared_content/custom_env_with_shared_content_test.go b/examples/custom_env_with_shared_content/custom_env_with_shared_content_test.go index df770dc..af9a2b0 100644 --- a/examples/custom_env_with_shared_content/custom_env_with_shared_content_test.go +++ b/examples/custom_env_with_shared_content/custom_env_with_shared_content_test.go @@ -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 }) } diff --git a/examples/simple/http_server_test.go b/examples/simple/http_server_test.go index 6e8f278..f5e8614 100644 --- a/examples/simple/http_server_test.go +++ b/examples/simple/http_server_test.go @@ -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) } diff --git a/interface.go b/interface.go index c9b1965..c9c4e25 100644 --- a/interface.go +++ b/interface.go @@ -11,6 +11,11 @@ type Env interface { // f call exactly once for every combination of scope and params // params must be json serializable (deserialize not need) Cache(params interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{} + + // CacheWithCleanup cache result of f calls + // f call exactly once for every combination of scope and params + // params must be json serializable (deserialize not need) + CacheWithCleanup(params interface{}, opt *FixtureOptions, f FixtureCallbackWithCleanupFunc) interface{} } var ( @@ -48,8 +53,17 @@ const ( // then cache error about unexpected exit type FixtureCallbackFunc func() (res interface{}, err error) +// FixtureCallbackWithCleanupFunc - function, which result can cached +// res - result for cache. +// cleanup - if not nil - call on fixture cleanup. It called exactly once for every successfully call fixture +// if err not nil - T().Fatalf() will called with error message +// if res exit without return (panic, GoExit, t.FailNow, ...) +// then cache error about unexpected exit +type FixtureCallbackWithCleanupFunc func() (res interface{}, cleanup FixtureCleanupFunc, err error) + // FixtureCleanupFunc - callback function for cleanup after // fixture value out from lifetime scope +// it called exactly once for every succesully call fixture type FixtureCleanupFunc func() // FixtureOptions options for fixenv engine @@ -58,9 +72,9 @@ type FixtureOptions struct { // Scope for cache result Scope CacheScope - // CleanupFunc if not nil - called for cleanup fixture results - // it called exactly once for every succesully call fixture - CleanupFunc FixtureCleanupFunc + // cleanupFunc if not nil - called for cleanup fixture results + // internal implementation details + cleanupFunc FixtureCleanupFunc } // T is subtype of testing.TB From 9eb4df05a984d2cc7db0b16be7a8d54b8c52dc1d Mon Sep 17 00:00:00 2001 From: rekby Date: Sun, 12 Jun 2022 15:20:45 +0300 Subject: [PATCH 2/5] use testEnv without global state --- env_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/env_test.go b/env_test.go index e470144..3d9e6f2 100644 --- a/env_test.go +++ b/env_test.go @@ -282,7 +282,7 @@ 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 := NewEnv(tMock) + env := newTestEnv(tMock) callbackCalled := 0 var callbackFunc FixtureCallbackWithCleanupFunc = func() (res interface{}, cleanup FixtureCleanupFunc, err error) { @@ -302,7 +302,7 @@ func Test_Env_CacheWithCleanup(t *testing.T) { t.Run("WithCleanup", func(t *testing.T) { tMock := &testMock{name: t.Name()} - env := NewEnv(tMock) + env := newTestEnv(tMock) callbackCalled := 0 cleanupCalled := 0 From e4809180f7f39383c0b4bab0716880f7eb3bd4d0 Mon Sep 17 00:00:00 2001 From: rekby Date: Sun, 12 Jun 2022 15:35:15 +0300 Subject: [PATCH 3/5] fix tests --- .github/workflows/go.yml | 7 ++++++- env_generic_sugar_test.go | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5034952..07e15c8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -29,8 +29,13 @@ jobs: run: go build -v ./... - name: Test + if: ${{ matrix.goVersion != env.GO_VERSION }} + 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: diff --git a/env_generic_sugar_test.go b/env_generic_sugar_test.go index 66d2d20..c572d7f 100644 --- a/env_generic_sugar_test.go +++ b/env_generic_sugar_test.go @@ -26,6 +26,29 @@ 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{} onCacheWithCleanup func(params interface{}, opt *FixtureOptions, f FixtureCallbackWithCleanupFunc) interface{} From 086a288d249147f26dcb080fa1a25f38ed8cc9c8 Mon Sep 17 00:00:00 2001 From: rekby Date: Sun, 12 Jun 2022 15:38:38 +0300 Subject: [PATCH 4/5] remove unused code --- env.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/env.go b/env.go index b8727e7..bb4442c 100644 --- a/env.go +++ b/env.go @@ -81,9 +81,6 @@ func (e *EnvT) CacheWithCleanup(params interface{}, opt *FixtureOptions, f Fixtu if opt == nil { opt = &FixtureOptions{} } - if opt.cleanupFunc != nil { - e.t.Fatalf("CacheWithCleanup not compatible with CleanupFunc param") - } var resCleanupFunc FixtureCleanupFunc From c86d47b6a35305a9e1875dc124f17c5b370abd39 Mon Sep 17 00:00:00 2001 From: rekby Date: Sun, 12 Jun 2022 15:40:05 +0300 Subject: [PATCH 5/5] simple test for latest golang too --- .github/workflows/go.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 07e15c8..87c9e6d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -29,7 +29,6 @@ jobs: run: go build -v ./... - name: Test - if: ${{ matrix.goVersion != env.GO_VERSION }} run: go test ./... - name: Test with coverage profiler