-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: added experimental test aware provider
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
Showing
3 changed files
with
286 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{}) | ||
}) | ||
} |