Skip to content

Commit

Permalink
test: added experimental test aware provider
Browse files Browse the repository at this point in the history
Added simple version of a "TestAware" provider that is managing a TestName=>FeatureProvider map and knows which feature provider to delegate the methods calls for a given test.

Signed-off-by: Bernd Warmuth <[email protected]>
  • Loading branch information
warber committed Oct 23, 2024
1 parent 0b13f8a commit 356940d
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 20 deletions.
20 changes: 0 additions & 20 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
github.com/cucumber/godog v0.14.0 h1:h/K4t7XBxsFBF+UJEahNqJ1/2VHVepRXCSq3WWWnehs=
github.com/cucumber/godog v0.14.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces=
github.com/cucumber/godog v0.14.1 h1:HGZhcOyyfaKclHjJ+r/q93iaTJZLKYW6Tv3HkmUE6+M=
github.com/cucumber/godog v0.14.1/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces=
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
Expand All @@ -11,12 +9,9 @@ github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
Expand All @@ -31,7 +26,6 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
Expand All @@ -57,12 +51,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
Expand All @@ -79,14 +67,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
108 changes: 108 additions & 0 deletions pkg/openfeaturetest/testprovider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package openfeaturetest

import (
"context"
"fmt"
"github.com/open-feature/go-sdk/openfeature"
"runtime"
"sync"
"testing"
)

const testNameKey = "testName"

// NewTestAwareProvider creates a new `TestAwareProvider`
func NewTestAwareProvider() TestAwareProvider {
return TestAwareProvider{
providers: &sync.Map{},
}
}

// TestAwareProvider can be used in parallel unit tests. It holds a map of unit test name to `openfeature.FeatureProvider`s.
// Before executing the test, specify the actual (in memory) provider that's going to be used for the specific test using the
// `SetProvider` method.
type TestAwareProvider struct {
openfeature.NoopProvider
providers *sync.Map
}

// SetProvider sets a given `FeatureProvider` for a given test.
func (tp TestAwareProvider) SetProvider(test *testing.T, fp openfeature.FeatureProvider) {
storeGoroutineLocal(testNameKey, test.Name())
tp.providers.Store(test.Name(), fp)
}

func (tp TestAwareProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
return tp.getProvider().BooleanEvaluation(ctx, flag, defaultValue, flCtx)
}

func (tp TestAwareProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, flCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
return tp.getProvider().StringEvaluation(ctx, flag, defaultValue, flCtx)
}

func (tp TestAwareProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
return tp.getProvider().FloatEvaluation(ctx, flag, defaultValue, flCtx)
}

func (tp TestAwareProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, flCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
return tp.getProvider().IntEvaluation(ctx, flag, defaultValue, flCtx)
}

func (tp TestAwareProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, flCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
return tp.getProvider().ObjectEvaluation(ctx, flag, defaultValue, flCtx)
}

func (tp TestAwareProvider) Hooks() []openfeature.Hook {
return tp.NoopProvider.Hooks()
}

func (tp TestAwareProvider) Metadata() openfeature.Metadata {
return tp.NoopProvider.Metadata()
}

func (tp TestAwareProvider) getProvider() openfeature.FeatureProvider {
// Retrieve the test name from the goroutine-local storage.
testName, ok := getGoroutineLocal(testNameKey).(string)
if !ok {
panic("unable to detect test name")
}

// Load the feature provider corresponding to the test name.
provider, ok := tp.providers.Load(testName)
if !ok {
panic("unable to find feature provider for given test name: " + testName)
}

// Assert that the loaded provider is of type openfeature.FeatureProvider.
featureProvider, ok := provider.(openfeature.FeatureProvider)
if !ok {
panic("invalid type for feature provider for given test name: " + testName)
}

return featureProvider
}

var goroutineLocalData sync.Map

func storeGoroutineLocal(key, value interface{}) {
gID := getGoroutineID()
goroutineLocalData.Store(fmt.Sprintf("%d_%v", gID, key), value)
}

func getGoroutineLocal(key interface{}) interface{} {
gID := getGoroutineID()
value, _ := goroutineLocalData.Load(fmt.Sprintf("%d_%v", gID, key))
return value
}

func getGoroutineID() uint64 {
var buf [64]byte
n := runtime.Stack(buf[:], false)
stackLine := string(buf[:n])
var gID uint64
_, err := fmt.Sscanf(stackLine, "goroutine %d ", &gID)
if err != nil {
panic("unable to extract GID from stack trace")
}
return gID
}
178 changes: 178 additions & 0 deletions pkg/openfeaturetest/testprovider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package openfeaturetest

import (
"context"
"github.com/open-feature/go-sdk/openfeature"
"github.com/open-feature/go-sdk/openfeature/memprovider"
"testing"
)

func TestParallelSingletonUsage(t *testing.T) {
t.Parallel()

testProvider := NewTestAwareProvider()
err := openfeature.GetApiInstance().SetProvider(testProvider)
if err != nil {
t.Errorf("unable to set provider on TestAwareProvider")
}

tests := map[string]struct {
givenProvider openfeature.FeatureProvider
want bool
}{
"test when flag is true": {
givenProvider: memprovider.NewInMemoryProvider(
map[string]memprovider.InMemoryFlag{
"some_cool_feature": {
State: memprovider.Enabled,
DefaultVariant: "variant_1",
Variants: map[string]any{
"variant_1": true,
},
ContextEvaluator: nil,
},
},
),
want: true,
},
"test when flag is false": {
givenProvider: memprovider.NewInMemoryProvider(
map[string]memprovider.InMemoryFlag{
"some_cool_feature": {
State: memprovider.Enabled,
DefaultVariant: "variant_1",
Variants: map[string]any{
"variant_1": false,
},
ContextEvaluator: nil,
},
"f2": {
State: memprovider.Enabled,
DefaultVariant: "variant_1",
Variants: map[string]any{
"variant_1": "v1",
},
ContextEvaluator: nil,
},
},
),
want: false,
},
}

for name, tt := range tests {
tt := tt
name := name
t.Run(name, func(t *testing.T) {
t.Parallel()
testProvider.SetProvider(t, tt.givenProvider)

// < CODE UNDER TEST >
got := openfeature.GetApiInstance().
GetClient().
Boolean(context.TODO(), "some_cool_feature", false, openfeature.EvaluationContext{})
// </ CODE UNDER TEST >

if got != tt.want {
t.Fatalf("uh oh, value is not as expected: got %v, want %v", got, tt.want)
}
})
}
}

func TestTestAwareProvider(t *testing.T) {
taw := NewTestAwareProvider()

memProvider := memprovider.NewInMemoryProvider(
map[string]memprovider.InMemoryFlag{
"ff-bool": {
State: memprovider.Enabled,
DefaultVariant: "variant_1",
Variants: map[string]any{
"variant_1": true,
},
},
"ff-string": {
State: memprovider.Enabled,
DefaultVariant: "variant_1",
Variants: map[string]any{
"variant_1": "str",
},
},
"ff-int": {
State: memprovider.Enabled,
DefaultVariant: "variant_1",
Variants: map[string]any{
"variant_1": 1,
},
},
"ff-float": {
State: memprovider.Enabled,
DefaultVariant: "variant_1",
Variants: map[string]any{
"variant_1": float64(1),
},
},
"ff-obj": {
State: memprovider.Enabled,
DefaultVariant: "variant_1",
Variants: map[string]any{
"variant_1": "obj",
},
},
},
)

t.Run("test bool evaluation", func(t *testing.T) {
taw.SetProvider(t, memProvider)
result := taw.BooleanEvaluation(context.TODO(), "ff-bool", false, openfeature.FlattenedContext{})
if result.Value != true {
t.Errorf("got %v, want %v", result, true)
}
})

t.Run("test string evaluation", func(t *testing.T) {
taw.SetProvider(t, memProvider)
result := taw.StringEvaluation(context.TODO(), "ff-string", "otherStr", openfeature.FlattenedContext{})
if result.Value != "str" {
t.Errorf("got %v, want %v", result, true)
}
})

t.Run("test int evaluation", func(t *testing.T) {
taw.SetProvider(t, memProvider)
result := taw.IntEvaluation(context.TODO(), "ff-int", int64(2), openfeature.FlattenedContext{})
if result.Value != 1 {
t.Errorf("got %v, want %v", result, true)
}
})

t.Run("test float evaluation", func(t *testing.T) {
taw.SetProvider(t, memProvider)
result := taw.FloatEvaluation(context.TODO(), "ff-float", float64(2), openfeature.FlattenedContext{})
if result.Value != float64(1) {
t.Errorf("got %v, want %v", result, true)
}
})
t.Run("test obj evaluation", func(t *testing.T) {
taw.SetProvider(t, memProvider)
result := taw.ObjectEvaluation(context.TODO(), "ff-obj", "stringobj", openfeature.FlattenedContext{})
if result.Value != "obj" {
t.Errorf("got %v, want %v", result, true)
}
})
}

func Test_TestAwareProviderPanics(t *testing.T) {

t.Run("provider panics if no test name was provided by calling SetProvider()", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("the test aware provider did not panic")
}
}()

taw := NewTestAwareProvider()
taw.BooleanEvaluation(context.TODO(), "my-flag", true, openfeature.FlattenedContext{})
})
}

0 comments on commit 356940d

Please sign in to comment.