Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestDatafileCacheKeyForTargetAssertion(t *testing.T) {
}
}

func TestDatafileCacheKeyForTargetAssertionFallsBackToBase(t *testing.T) {
func TestDatafileCacheKeyForTargetAssertionDoesNotFallBackToBase(t *testing.T) {
cache := map[string]interface{}{
"production": map[string]interface{}{"kind": "base"},
}
Expand All @@ -60,8 +60,8 @@ func TestDatafileCacheKeyForTargetAssertionFallsBackToBase(t *testing.T) {
"target": "checkout",
}, cache)

if got != "production" {
t.Fatalf("expected production, got %s", got)
if got != "production-target-checkout" {
t.Fatalf("expected production-target-checkout, got %s", got)
}
}

Expand Down
223 changes: 142 additions & 81 deletions cmd/commands/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,114 @@ func valuesEqual(left, right interface{}) bool {
return reflect.DeepEqual(normalize(left), normalize(right))
}

// RunTestVariable tests a global variable assertion.
func RunTestVariable(assertion map[string]interface{}, variableKey string, instance *featurevisor.Featurevisor, level string) AssertionResult {
started := time.Now()
context := featurevisor.Context{}
if value, ok := assertion["context"].(map[string]interface{}); ok {
context = featurevisor.Context(value)
}
if raw, ok := assertion["stickyVariables"].(map[string]interface{}); ok {
sticky := featurevisor.StickyVariables{}
for key, value := range raw {
sticky[key] = value
}
instance.SetStickyVariables(sticky, true)
func formatTestValue(value interface{}) string {
raw, err := json.Marshal(value)
if err != nil {
return fmt.Sprintf("%v", value)
}
return string(raw)
}

type globalVariableEvaluator interface {
EvaluateGlobalVariable(variableKey string, args ...interface{}) featurevisor.Evaluation
}

func evaluateVariableAssertion(assertion map[string]interface{}, variableKey string, evaluator globalVariableEvaluator, prefix string) string {
options := featurevisor.OverrideOptions{}
if value, ok := assertion["defaultVariableValue"]; ok {
options.DefaultVariableValue = value
options.DefaultVariableValueSet = true
}
evaluation := instance.EvaluateGlobalVariable(variableKey, context, options)
evaluation := evaluator.EvaluateGlobalVariable(variableKey, featurevisor.Context{}, options)
errors := ""
if expected, ok := assertion["expectedValue"]; ok && !valuesEqual(evaluation.VariableValue, expected) {
errors += fmt.Sprintf(" ✘ expectedValue: expected %v but received %v\n", expected, evaluation.VariableValue)
errors += fmt.Sprintf(" ✘ %sexpectedValue: expected %s but received %s\n", prefix, formatTestValue(expected), formatTestValue(evaluation.VariableValue))
}
if expectedFields, ok := assertion["expectedEvaluation"].(map[string]interface{}); ok {
for key, expected := range expectedFields {
actual := getEvaluationValue(evaluation, key)
if !valuesEqual(actual, expected) {
errors += fmt.Sprintf(" ✘ expectedEvaluation.%s: expected %v but received %v\n", key, expected, actual)
errors += fmt.Sprintf(" ✘ %sexpectedEvaluation.%s: expected %s but received %s\n", prefix, key, formatTestValue(expected), formatTestValue(actual))
}
}
}
return errors
}

// RunTestVariable tests a global variable assertion.
func RunTestVariable(assertion map[string]interface{}, variableKey string, instance *featurevisor.Featurevisor, level string) AssertionResult {
started := time.Now()
errors := evaluateVariableAssertion(assertion, variableKey, instance, "")
if children, ok := assertion["children"].([]interface{}); ok {
for index, rawChild := range children {
childAssertion, ok := rawChild.(map[string]interface{})
if !ok {
continue
}
childContext := contextFromAssertion(childAssertion)
childStickyFeatures := stickyFeaturesFromAssertion(childAssertion, "stickyFeatures")
childStickyVariables := stickyVariablesFromAssertion(childAssertion)
child := instance.Spawn(childContext, featurevisor.SpawnOptions{
StickyFeatures: childStickyFeatures,
StickyVariables: childStickyVariables,
})
func() {
defer child.Close()
errors += evaluateVariableAssertion(childAssertion, variableKey, child, fmt.Sprintf("children[%d].", index))
}()
}
}
return AssertionResult{HasError: errors != "", Errors: errors, Duration: time.Since(started).Seconds()}
}

func contextFromAssertion(assertion map[string]interface{}) featurevisor.Context {
if value, ok := assertion["context"].(map[string]interface{}); ok {
return featurevisor.Context(value)
}
return featurevisor.Context{}
}

func stickyVariablesFromAssertion(assertion map[string]interface{}) *featurevisor.StickyVariables {
raw, ok := assertion["stickyVariables"].(map[string]interface{})
if !ok {
return nil
}
sticky := featurevisor.StickyVariables{}
for key, value := range raw {
sticky[key] = value
}
return &sticky
}

func stickyFeaturesFromAssertion(assertion map[string]interface{}, field string) *featurevisor.StickyFeatures {
raw, ok := assertion[field].(map[string]interface{})
if !ok {
return nil
}
sticky := featurevisor.StickyFeatures{}
for key, value := range raw {
featureValue, ok := value.(map[string]interface{})
if !ok {
continue
}
evaluated := featurevisor.EvaluatedFeature{}
if enabled, ok := featureValue["enabled"].(bool); ok {
evaluated.Enabled = enabled
}
if variation, ok := featureValue["variation"].(string); ok {
evaluated.Variation = &variation
}
if variables, ok := featureValue["variables"].(map[string]interface{}); ok {
evaluated.Variables = map[featurevisor.VariableKey]featurevisor.VariableValue{}
for variableKey, variableValue := range variables {
evaluated.Variables[featurevisor.VariableKey(variableKey)] = variableValue
}
}
sticky[featurevisor.FeatureKey(key)] = evaluated
}
return &sticky
}

// TestFeature tests a feature with the given assertion
func RunTestFeature(assertion map[string]interface{}, featureKey string, instance *featurevisor.Featurevisor, level string) AssertionResult {
context := featurevisor.Context{}
Expand Down Expand Up @@ -234,42 +307,13 @@ func RunTestFeature(assertion map[string]interface{}, featureKey string, instanc
childContext = featurevisor.Context(childCtx)
}

// Create override options for child with sticky values
childOverrideOptions := featurevisor.OverrideOptions{
DefaultVariationValue: getDefaultVariationValue(childMap),
}

// Pass sticky values to child instance (matching TypeScript implementation)
childInstance := instance.Spawn(childContext, childOverrideOptions)

// Set sticky values for child if they exist
if sticky, ok := assertion["sticky"].(map[string]interface{}); ok {
// Convert sticky to proper format
stickyFeatures := featurevisor.StickyFeatures{}
for key, value := range sticky {
if featureSticky, ok := value.(map[string]interface{}); ok {
evaluatedFeature := featurevisor.EvaluatedFeature{}
if enabled, ok := featureSticky["enabled"].(bool); ok {
evaluatedFeature.Enabled = enabled
}
if variation, ok := featureSticky["variation"]; ok {
if variationStr, ok := variation.(string); ok {
evaluatedFeature.Variation = &variationStr
}
}
if variables, ok := featureSticky["variables"].(map[string]interface{}); ok {
evaluatedFeature.Variables = make(map[featurevisor.VariableKey]featurevisor.VariableValue)
for varKey, varValue := range variables {
evaluatedFeature.Variables[featurevisor.VariableKey(varKey)] = varValue
}
}
stickyFeatures[featurevisor.FeatureKey(key)] = evaluatedFeature
}
}
childInstance.SetStickyFeatures(stickyFeatures, false)
}

childResult := RunTestFeatureChild(childMap, featureKey, childInstance, level)
childInstance := instance.Spawn(childContext, featurevisor.SpawnOptions{
StickyFeatures: stickyFeaturesFromAssertion(assertion, "sticky"),
})
childResult := func() AssertionResult {
defer childInstance.Close()
return RunTestFeatureChild(childMap, featureKey, childInstance, level)
}()

if childResult.HasError {
hasError = true
Expand Down Expand Up @@ -850,16 +894,11 @@ func datafileCacheKeyForAssertion(assertion map[string]interface{}, datafileCach
}
}

selectedDatafileKey := datafileCacheKey(environment)

if targetValue, ok := assertion["target"].(string); ok && targetValue != "" {
targetKey := targetDatafileCacheKey(environment, targetValue)
if _, exists := datafileCache[targetKey]; exists {
selectedDatafileKey = targetKey
}
return targetDatafileCacheKey(environment, targetValue)
}

return selectedDatafileKey
return datafileCacheKey(environment)
}

func buildDatafileCache(
Expand Down Expand Up @@ -945,8 +984,11 @@ func buildInstanceForAssertion(datafile interface{}, level string, assertion map

levelStr := featurevisor.LogLevel(level)
return featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Datafile: datafileContent,
LogLevel: &levelStr,
Datafile: datafileContent,
LogLevel: &levelStr,
Context: contextFromAssertion(assertion),
StickyFeatures: stickyFeaturesFromAssertion(assertion, "stickyFeatures"),
StickyVariables: stickyVariablesFromAssertion(assertion),
Modules: []*featurevisor.FeaturevisorModule{
{
Name: "tester-module",
Expand Down Expand Up @@ -1040,33 +1082,39 @@ func runTest(opts CLIOptions) {

datafile, ok := datafileCache[selectedDatafileKey]
if !ok {
fmt.Printf("missing datafile for key: %s\n", selectedDatafileKey)
os.Exit(1)
}

effectiveAssertion := cloneAssertion(assertionMap)
instance := buildInstanceForAssertion(datafile, level, effectiveAssertion)
testResult = missingDatafileAssertion(assertionMap, selectedDatafileKey)
} else {

effectiveAssertion := cloneAssertion(assertionMap)
instance := buildInstanceForAssertion(datafile, level, effectiveAssertion)

// Show datafile if requested (matching TypeScript implementation)
if opts.ShowDatafile {
fmt.Println("")
datafileJSON, _ := json.MarshalIndent(datafile, "", " ")
fmt.Println(string(datafileJSON))
fmt.Println("")
}

// Show datafile if requested (matching TypeScript implementation)
if opts.ShowDatafile {
fmt.Println("")
datafileJSON, _ := json.MarshalIndent(datafile, "", " ")
fmt.Println(string(datafileJSON))
fmt.Println("")
testResult = RunTestFeature(effectiveAssertion, test["feature"].(string), instance, level)
instance.Close()
}

testResult = RunTestFeature(effectiveAssertion, test["feature"].(string), instance, level)
instance.Close()
} else if variableKey, hasVariable := test["variable"].(string); hasVariable {
selectedDatafileKey := datafileCacheKeyForAssertion(assertionMap, datafileCache)
datafile, ok := datafileCache[selectedDatafileKey]
if !ok {
fmt.Printf("missing datafile for key: %s\n", selectedDatafileKey)
os.Exit(1)
testResult = missingDatafileAssertion(assertionMap, selectedDatafileKey)
} else {
if opts.ShowDatafile {
fmt.Println("")
datafileJSON, _ := json.MarshalIndent(datafile, "", " ")
fmt.Println(string(datafileJSON))
fmt.Println("")
}
instance := buildInstanceForAssertion(datafile, level, assertionMap)
testResult = RunTestVariable(assertionMap, variableKey, instance, level)
instance.Close()
}
instance := buildInstanceForAssertion(datafile, level, assertionMap)
testResult = RunTestVariable(assertionMap, variableKey, instance, level)
instance.Close()
} else if _, hasSegment := test["segment"]; hasSegment {
segmentKey := test["segment"].(string)
segment := segmentsByKey[segmentKey]
Expand Down Expand Up @@ -1116,6 +1164,19 @@ func runTest(opts CLIOptions) {
}
}

func missingDatafileAssertion(assertion map[string]interface{}, cacheKey string) AssertionResult {
environment, _ := assertion["environment"].(string)
if environment == "" {
environment = "none"
}
target, _ := assertion["target"].(string)
message := fmt.Sprintf("datafile not found for environment %q", environment)
if target != "" {
message += fmt.Sprintf(" and target %q", target)
}
return AssertionResult{HasError: true, Errors: fmt.Sprintf(" ✘ %s (cache key: %s)\n", message, cacheKey)}
}

func convertToStringSlice(interfaces []interface{}) []string {
strings := make([]string, len(interfaces))
for i, v := range interfaces {
Expand Down
7 changes: 4 additions & 3 deletions evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,16 +322,17 @@ func evaluate(options EvaluateOptions) Evaluation {
if options.sticky != nil {
if stickyFeature, exists := (*options.sticky)[options.FeatureKey]; exists {
// flag
if options.Type == EvaluationTypeFlag && stickyFeature.Enabled {
if options.Type == EvaluationTypeFlag {
enabled := stickyFeature.Enabled
evaluation = Evaluation{
Type: options.Type,
FeatureKey: options.FeatureKey,
Reason: EvaluationReasonSticky,
Sticky: &stickyFeature,
Enabled: &[]bool{true}[0],
Enabled: &enabled,
}

options.diagnosticReporter.Debug("using sticky enabled", logDetails{
options.diagnosticReporter.Debug("using sticky feature", logDetails{
"evaluation": evaluation,
})

Expand Down
19 changes: 19 additions & 0 deletions instance_sticky_features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ func TestStickyFeaturesInitialization(t *testing.T) {
}
}

func TestStickyDisabledFeatureIsHonoured(t *testing.T) {
instance := CreateFeaturevisor(FeaturevisorOptions{
Datafile: DatafileContent{
SchemaVersion: "2",
Revision: "sticky-disabled",
Segments: map[SegmentKey]Segment{},
Features: map[FeatureKey]Feature{
"test": {BucketBy: []string{"userId"}},
},
},
StickyFeatures: &StickyFeatures{"test": {Enabled: false}},
})
defer instance.Close()

if instance.IsEnabled("test", Context{"userId": "123"}) {
t.Fatal("expected sticky disabled feature to remain disabled")
}
}

// TestSetStickyVariadicSignature tests that the new variadic signature works correctly
func TestSetStickyVariadicSignature(t *testing.T) {
instance := CreateFeaturevisor(FeaturevisorOptions{})
Expand Down
2 changes: 1 addition & 1 deletion openfeature/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/featurevisor/featurevisor-go/openfeature/v3
go 1.25.0

require (
github.com/featurevisor/featurevisor-go/v3 v3.0.0
github.com/featurevisor/featurevisor-go/v3 v3.1.0
github.com/open-feature/go-sdk v1.17.2
)

Expand Down
Loading