Skip to content

Commit

Permalink
Merge pull request #34 Move cache to one function
Browse files Browse the repository at this point in the history
  • Loading branch information
rekby authored Sep 24, 2023
2 parents b1b10c2 + fade6db commit def74bd
Show file tree
Hide file tree
Showing 18 changed files with 407 additions and 42 deletions.
41 changes: 41 additions & 0 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ func (e *EnvT) T() T {
// opt - fixture options, nil for default options.
// f - callback - fixture body.
// Cache guarantee for call f exactly once for same Cache called and params combination.
// Deprecated: will be removed in next versions.
// Use EnvT.CacheResult instead
func (e *EnvT) Cache(cacheKey interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{} {
return e.cache(cacheKey, opt, f)
}
Expand All @@ -99,6 +101,8 @@ func (e *EnvT) Cache(cacheKey interface{}, opt *FixtureOptions, f FixtureCallbac
// 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.
// Deprecated: will be removed in next versions.
// Use EnvT.CacheResult instead
func (e *EnvT) CacheWithCleanup(cacheKey interface{}, opt *FixtureOptions, f FixtureCallbackWithCleanupFunc) interface{} {
if opt == nil {
opt = &FixtureOptions{}
Expand All @@ -119,6 +123,43 @@ func (e *EnvT) CacheWithCleanup(cacheKey interface{}, opt *FixtureOptions, f Fix
return e.cache(cacheKey, opt, fWithoutCleanup)
}

// CacheResult call f callback once and cache result (ok and error),
// then return same result for all calls of the callback without additional calls
// f with same options calls max once per test (or defined test scope)
func (e *EnvT) CacheResult(f FixtureFunction, options ...CacheOptions) interface{} {
var cacheOptions CacheOptions
switch len(options) {
case 0:
cacheOptions = CacheOptions{}
case 1:
cacheOptions = options[0]
default:
panic(fmt.Errorf("max len of cache result cacheOptions is 1, given: %v", len(options)))
}

var resCleanupFunc FixtureCleanupFunc

var fWithoutCleanup FixtureCallbackFunc = func() (res interface{}, err error) {
result, err := f()
resCleanupFunc = result.Cleanup
return result.Value, err
}

opt := &FixtureOptions{}
opt.Scope = cacheOptions.Scope
opt.additionlSkipExternalCalls = cacheOptions.additionlSkipExternalCalls
opt.cleanupFunc = resCleanupFunc

opt.cleanupFunc = func() {
if resCleanupFunc != nil {
resCleanupFunc()
}
}

return e.cache(cacheOptions.CacheKey, opt, fWithoutCleanup)

}

// cache must be call from first-level public function
// UserFunction->EnvFunction->cache for good determine caller name
func (e *EnvT) cache(cacheKey interface{}, opt *FixtureOptions, f FixtureCallbackFunc) interface{} {
Expand Down
62 changes: 62 additions & 0 deletions env_generic_sugar.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

package fixenv

import "fmt"

// Cache is call f once per cache scope (default per test) and cache result (success or error).
// All other calls of the f will return same result
// Deprecated: Use CacheResult
func Cache[TRes any](env Env, cacheKey any, opt *FixtureOptions, f func() (TRes, error)) TRes {
addSkipLevel(&opt)
callbackResult := env.Cache(cacheKey, opt, func() (res interface{}, err error) {
Expand All @@ -16,6 +21,10 @@ func Cache[TRes any](env Env, cacheKey any, opt *FixtureOptions, f func() (TRes,
return res
}

// CacheWithCleanup is call f once per cache scope (default per test) and cache result (success or error).
// All other calls of the f will return same result.
// Used when fixture need own cleanup after exit from test scope
// Deprecated: Use CacheResult
func CacheWithCleanup[TRes any](env Env, cacheKey any, opt *FixtureOptions, f func() (TRes, FixtureCleanupFunc, error)) TRes {
addSkipLevel(&opt)
callbackResult := env.CacheWithCleanup(cacheKey, opt, func() (res interface{}, cleanup FixtureCleanupFunc, err error) {
Expand All @@ -29,9 +38,62 @@ func CacheWithCleanup[TRes any](env Env, cacheKey any, opt *FixtureOptions, f fu
return res
}

// CacheResult is call f once per cache scope (default per test) and cache result (success or error).
// All other calls of the f will return same result.
func CacheResult[TRes any](env Env, f GenericFixtureFunction[TRes], options ...CacheOptions) TRes {
var cacheOptions CacheOptions
switch len(options) {
case 0:
cacheOptions = CacheOptions{}
case 1:
cacheOptions = options[0]
default:
panic(fmt.Errorf("max len of cache result cacheOptions is 1, given: %v", len(options)))
}

addSkipLevelCache(&cacheOptions)
var oldStyleFunc FixtureFunction = func() (*Result, error) {
res, err := f()

var oldStyleRes *Result
if res != nil {
oldStyleRes = &Result{
Value: res.Value,
ResultAdditional: res.ResultAdditional,
}
}
return oldStyleRes, err
}
res := env.CacheResult(oldStyleFunc, cacheOptions)
return res.(TRes)
}

// GenericFixtureFunction - callback function with structured result
type GenericFixtureFunction[ResT any] func() (*GenericResult[ResT], error)

// GenericResult of fixture callback
type GenericResult[ResT any] struct {
Value ResT
ResultAdditional
}

// NewGenericResult return result struct and nil error.
// Use it for smaller boilerplate for define generic specifications
func NewGenericResult[ResT any](res ResT) *GenericResult[ResT] {
return &GenericResult[ResT]{Value: res}
}

func NewGenericResultWithCleanup[ResT any](res ResT, cleanup FixtureCleanupFunc) *GenericResult[ResT] {
return &GenericResult[ResT]{Value: res, ResultAdditional: ResultAdditional{Cleanup: cleanup}}
}

func addSkipLevel(optspp **FixtureOptions) {
if *optspp == nil {
*optspp = &FixtureOptions{}
}
(*optspp).additionlSkipExternalCalls++
}

func addSkipLevelCache(optspp *CacheOptions) {
(*optspp).additionlSkipExternalCalls++
}
112 changes: 112 additions & 0 deletions env_generic_sugar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
package fixenv

import (
"fmt"
"github.com/rekby/fixenv/internal"
"github.com/stretchr/testify/assert"
"math/rand"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -89,12 +92,108 @@ func TestCacheWithCleanupGeneric(t *testing.T) {
require.Equal(t, 1, f1())
require.Equal(t, 2, f2())
})
}

func TestCacheResultGeneric(t *testing.T) {
t.Run("PassParams", func(t *testing.T) {
inOpt := CacheOptions{
CacheKey: 123,
Scope: ScopeTest,
}

cleanupCalledBack := 0

env := envMock{onCacheResult: func(opt CacheOptions, f FixtureFunction) interface{} {
opt.additionlSkipExternalCalls--
require.Equal(t, inOpt, opt)
res, _ := f()
return res.Value
}}

f := func() (*GenericResult[int], error) {
cleanup := func() {
cleanupCalledBack++
}
return NewGenericResultWithCleanup(2, cleanup), nil
}
res := CacheResult(env, f, inOpt)
require.Equal(t, 2, res)
})
t.Run("SkipAdditionalCache", func(t *testing.T) {
test := &internal.TestMock{TestName: t.Name()}
env := newTestEnv(test)

f1 := func() int {
return CacheResult(env, func() (*GenericResult[int], error) {
return NewGenericResult(1), nil
})
}
f2 := func() int {
return CacheResult(env, func() (*GenericResult[int], error) {
return NewGenericResult(2), nil
})
}

require.Equal(t, 1, f1())
require.Equal(t, 2, f2())
})
}

func TestCacheResultPanic(t *testing.T) {
t.Run("Simple", func(t *testing.T) {
at := assert.New(t)
tMock := &internal.TestMock{TestName: "mock", SkipGoexit: true}
e := New(tMock)

rndFix := func(e Env) int {
return CacheResult(e, func() (*GenericResult[int], error) {
return NewGenericResult(rand.Int()), nil
})
}
first := rndFix(e)
second := rndFix(e)

at.Equal(first, second)
})
t.Run("Options", func(t *testing.T) {
at := assert.New(t)
tMock := &internal.TestMock{TestName: "mock", SkipGoexit: true}
e := New(tMock)

rndFix := func(e Env, name string) int {
return CacheResult(e, func() (*GenericResult[int], error) {
return NewGenericResult(rand.Int()), nil
}, CacheOptions{CacheKey: name})
}
first1 := rndFix(e, "first")
first2 := rndFix(e, "first")
second1 := rndFix(e, "second")
second2 := rndFix(e, "second")

at.Equal(first1, first2)
at.Equal(second1, second2)
at.NotEqual(first1, second1)
})
t.Run("Panic", func(t *testing.T) {
at := assert.New(t)
tMock := &internal.TestMock{TestName: "mock", SkipGoexit: true}
e := New(tMock)

rndFix := func(e Env, name string) int {
return CacheResult(e, func() (*GenericResult[int], error) {
return NewGenericResult(rand.Int()), nil
}, CacheOptions{CacheKey: name}, CacheOptions{CacheKey: name})
}
at.Panics(func() {
rndFix(e, "first")
})
})
}

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

func (e envMock) T() T {
Expand All @@ -108,3 +207,16 @@ func (e envMock) Cache(params interface{}, opt *FixtureOptions, f FixtureCallbac
func (e envMock) CacheWithCleanup(params interface{}, opt *FixtureOptions, f FixtureCallbackWithCleanupFunc) interface{} {
return e.onCacheWithCleanup(params, opt, f)
}

func (e envMock) CacheResult(f FixtureFunction, options ...CacheOptions) interface{} {
var opts CacheOptions
switch len(options) {
case 0:
// pass
case 1:
opts = options[0]
default:
panic(fmt.Errorf("max options len is 1, given: %v", len(options)))
}
return e.onCacheResult(opts, f)
}
82 changes: 81 additions & 1 deletion env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fixenv
import (
"errors"
"github.com/rekby/fixenv/internal"
"math/rand"
"runtime"
"sync"
"testing"
Expand Down Expand Up @@ -301,6 +302,86 @@ func Test_Env_CacheWithCleanup(t *testing.T) {
})
}

func Test_Env_CacheResult(t *testing.T) {
t.Run("Simple", func(t *testing.T) {
at := assert.New(t)
tMock := &internal.TestMock{TestName: "mock", SkipGoexit: true}
e := New(tMock)

rndFix := func(e Env) int {
return e.CacheResult(func() (*Result, error) {
return NewResult(rand.Int()), nil
}).(int)
}
first := rndFix(e)
second := rndFix(e)

at.Equal(first, second)
})
t.Run("Options", func(t *testing.T) {
at := assert.New(t)
tMock := &internal.TestMock{TestName: "mock", SkipGoexit: true}
e := New(tMock)

rndFix := func(e Env, name string) int {
return e.CacheResult(func() (*Result, error) {
return NewResult(rand.Int()), nil
}, CacheOptions{CacheKey: name}).(int)
}
first1 := rndFix(e, "first")
first2 := rndFix(e, "first")
second1 := rndFix(e, "second")
second2 := rndFix(e, "second")

at.Equal(first1, first2)
at.Equal(second1, second2)
at.NotEqual(first1, second1)
})
t.Run("WithCleanup", func(t *testing.T) {
tMock := &internal.TestMock{TestName: t.Name()}
env := newTestEnv(tMock)

callbackCalled := 0
cleanupCalled := 0
var callbackFunc FixtureFunction = func() (*Result, error) {
callbackCalled++
cleanup := func() {
cleanupCalled++
}
return NewResultWithCleanup(callbackCalled, cleanup), nil
}

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

// got value from cache
res = env.CacheResult(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)
})
t.Run("Panic", func(t *testing.T) {
at := assert.New(t)
tMock := &internal.TestMock{TestName: "mock", SkipGoexit: true}
e := New(tMock)

rndFix := func(e Env, name string) int {
return e.CacheResult(func() (*Result, error) {
return NewResult(rand.Int()), nil
}, CacheOptions{CacheKey: name}, CacheOptions{CacheKey: name}).(int)
}
at.Panics(func() {
rndFix(e, "first")
})
})
}

func Test_FixtureWrapper(t *testing.T) {
t.Run("ok", func(t *testing.T) {
at := assert.New(t)
Expand Down Expand Up @@ -651,7 +732,6 @@ func TestNewEnv(t *testing.T) {
tm.SkipGoexit = true
New(tm)

//goland:noinspection GoDeprecation
NewEnv(tm)
if len(tm.Fatals) == 0 {
t.Fatal("bad double login between new and NewEnv")
Expand Down
Loading

0 comments on commit def74bd

Please sign in to comment.