...

Source file src/github.com/launchdarkly/go-server-sdk-evaluation/v2/evaluator_prerequisites_test.go

Documentation: github.com/launchdarkly/go-server-sdk-evaluation/v2

     1  package evaluation
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
     8  
     9  	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
    10  	"github.com/launchdarkly/go-sdk-common/v3/ldlogtest"
    11  	"github.com/launchdarkly/go-sdk-common/v3/ldreason"
    12  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    13  	m "github.com/launchdarkly/go-test-helpers/v3/matchers"
    14  
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  type prereqEventSink struct {
    20  	events []PrerequisiteFlagEvent
    21  }
    22  
    23  func (p *prereqEventSink) record(event PrerequisiteFlagEvent) {
    24  	p.events = append(p.events, event)
    25  }
    26  
    27  func TestFlagReturnsOffVariationIfPrerequisiteIsNotFound(t *testing.T) {
    28  	f0 := ldbuilders.NewFlagBuilder("feature0").
    29  		On(true).
    30  		OffVariation(1).
    31  		AddPrerequisite("feature1", 1).
    32  		FallthroughVariation(0).
    33  		Variations(fallthroughValue, offValue, onValue).
    34  		Build()
    35  	evaluator := NewEvaluator(basicDataProvider().withNonexistentFlag("feature1"))
    36  
    37  	eventSink := prereqEventSink{}
    38  	result := evaluator.Evaluate(&f0, flagTestContext, eventSink.record)
    39  	m.In(t).Assert(result, ResultDetailProps(1, offValue, ldreason.NewEvalReasonPrerequisiteFailed("feature1")))
    40  	assert.Equal(t, 0, len(eventSink.events))
    41  }
    42  
    43  func TestFlagReturnsOffVariationAndEventIfPrerequisiteIsOff(t *testing.T) {
    44  	f0 := ldbuilders.NewFlagBuilder("feature0").
    45  		On(true).
    46  		OffVariation(1).
    47  		AddPrerequisite("feature1", 1).
    48  		FallthroughVariation(0).
    49  		Variations(fallthroughValue, offValue, onValue).
    50  		Build()
    51  	f1 := ldbuilders.NewFlagBuilder("feature1").
    52  		On(false).
    53  		OffVariation(1).
    54  		// note that even though it returns the desired variation, it is still off and therefore not a match
    55  		FallthroughVariation(0).
    56  		Variations(ldvalue.String("nogo"), ldvalue.String("go")).
    57  		Build()
    58  	evaluator := NewEvaluator(basicDataProvider().withStoredFlags(f1))
    59  
    60  	eventSink := prereqEventSink{}
    61  	result := evaluator.Evaluate(&f0, flagTestContext, eventSink.record)
    62  	m.In(t).Assert(result, ResultDetailProps(1, offValue, ldreason.NewEvalReasonPrerequisiteFailed("feature1")))
    63  
    64  	assert.Equal(t, 1, len(eventSink.events))
    65  	e := eventSink.events[0]
    66  	assert.Equal(t, f0.Key, e.TargetFlagKey)
    67  	assert.Equal(t, flagTestContext, e.Context)
    68  	assert.Equal(t, &f1, e.PrerequisiteFlag)
    69  	m.In(t).Assert(e.PrerequisiteResult, ResultDetailProps(1, ldvalue.String("go"), ldreason.NewEvalReasonOff()))
    70  }
    71  
    72  func TestFlagReturnsOffVariationAndEventIfPrerequisiteIsNotMet(t *testing.T) {
    73  	f0 := ldbuilders.NewFlagBuilder("feature0").
    74  		On(true).
    75  		OffVariation(1).
    76  		AddPrerequisite("feature1", 1).
    77  		FallthroughVariation(0).
    78  		Variations(fallthroughValue, offValue, onValue).
    79  		Build()
    80  	f1 := ldbuilders.NewFlagBuilder("feature1").
    81  		On(true).
    82  		FallthroughVariation(0).
    83  		Variations(ldvalue.String("nogo"), ldvalue.String("go")).
    84  		Build()
    85  	evaluator := NewEvaluator(basicDataProvider().withStoredFlags(f1))
    86  
    87  	eventSink := prereqEventSink{}
    88  	result := evaluator.Evaluate(&f0, flagTestContext, eventSink.record)
    89  	m.In(t).Assert(result, ResultDetailProps(1, offValue, ldreason.NewEvalReasonPrerequisiteFailed("feature1")))
    90  
    91  	assert.Equal(t, 1, len(eventSink.events))
    92  	e := eventSink.events[0]
    93  	assert.Equal(t, f0.Key, e.TargetFlagKey)
    94  	assert.Equal(t, flagTestContext, e.Context)
    95  	assert.Equal(t, &f1, e.PrerequisiteFlag)
    96  	m.In(t).Assert(e.PrerequisiteResult, ResultDetailProps(0, ldvalue.String("nogo"), ldreason.NewEvalReasonFallthrough()))
    97  }
    98  
    99  func TestFlagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAreNoRules(t *testing.T) {
   100  	f0 := ldbuilders.NewFlagBuilder("feature0").
   101  		On(true).
   102  		OffVariation(1).
   103  		AddPrerequisite("feature1", 1).
   104  		FallthroughVariation(0).
   105  		Variations(fallthroughValue, offValue, onValue).
   106  		Build()
   107  	f1 := ldbuilders.NewFlagBuilder("feature1").
   108  		On(true).
   109  		FallthroughVariation(1). // this 1 matches the 1 in f0's prerequisites
   110  		Variations(ldvalue.String("nogo"), ldvalue.String("go")).
   111  		Build()
   112  	evaluator := NewEvaluator(basicDataProvider().withStoredFlags(f1))
   113  
   114  	eventSink := prereqEventSink{}
   115  	result := evaluator.Evaluate(&f0, flagTestContext, eventSink.record)
   116  	m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthrough()))
   117  
   118  	assert.Equal(t, 1, len(eventSink.events))
   119  	e := eventSink.events[0]
   120  	assert.Equal(t, f0.Key, e.TargetFlagKey)
   121  	assert.Equal(t, flagTestContext, e.Context)
   122  	assert.Equal(t, &f1, e.PrerequisiteFlag)
   123  	m.In(t).Assert(e.PrerequisiteResult, ResultDetailProps(1, ldvalue.String("go"), ldreason.NewEvalReasonFallthrough()))
   124  }
   125  
   126  func TestPrerequisiteCanMatchWithNonScalarValue(t *testing.T) {
   127  	f0 := ldbuilders.NewFlagBuilder("feature0").
   128  		On(true).
   129  		OffVariation(1).
   130  		AddPrerequisite("feature1", 1).
   131  		FallthroughVariation(0).
   132  		Variations(fallthroughValue, offValue, onValue).
   133  		Build()
   134  	prereqVar0 := ldvalue.ArrayOf(ldvalue.String("000"))
   135  	prereqVar1 := ldvalue.ArrayOf(ldvalue.String("001"))
   136  	f1 := ldbuilders.NewFlagBuilder("feature1").
   137  		On(true).
   138  		FallthroughVariation(1). // this 1 matches the 1 in f0's prerequisites
   139  		Variations(prereqVar0, prereqVar1).
   140  		Build()
   141  	evaluator := NewEvaluator(basicDataProvider().withStoredFlags(f1))
   142  
   143  	eventSink := prereqEventSink{}
   144  	result := evaluator.Evaluate(&f0, flagTestContext, eventSink.record)
   145  	m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthrough()))
   146  
   147  	assert.Equal(t, 1, len(eventSink.events))
   148  	e := eventSink.events[0]
   149  	assert.Equal(t, f0.Key, e.TargetFlagKey)
   150  	assert.Equal(t, flagTestContext, e.Context)
   151  	assert.Equal(t, &f1, e.PrerequisiteFlag)
   152  	m.In(t).Assert(e.PrerequisiteResult, ResultDetailProps(1, prereqVar1, ldreason.NewEvalReasonFallthrough()))
   153  }
   154  
   155  func TestMultipleLevelsOfPrerequisiteProduceMultipleEvents(t *testing.T) {
   156  	f0 := ldbuilders.NewFlagBuilder("feature0").
   157  		On(true).
   158  		OffVariation(1).
   159  		AddPrerequisite("feature1", 1).
   160  		FallthroughVariation(0).
   161  		Variations(fallthroughValue, offValue, onValue).
   162  		Build()
   163  	f1 := ldbuilders.NewFlagBuilder("feature1").
   164  		On(true).
   165  		AddPrerequisite("feature2", 1).
   166  		FallthroughVariation(1). // this 1 matches the 1 in f0's prerequisites
   167  		Variations(ldvalue.String("nogo"), ldvalue.String("go")).
   168  		Build()
   169  	f2 := ldbuilders.NewFlagBuilder("feature2").
   170  		On(true).
   171  		FallthroughVariation(1). // this 1 matches the 1 in f1's prerequisites
   172  		Variations(ldvalue.String("nogo"), ldvalue.String("go")).
   173  		Build()
   174  	evaluator := NewEvaluator(basicDataProvider().withStoredFlags(f1, f2))
   175  
   176  	eventSink := prereqEventSink{}
   177  	result := evaluator.Evaluate(&f0, flagTestContext, eventSink.record)
   178  	m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthrough()))
   179  
   180  	assert.Equal(t, 2, len(eventSink.events))
   181  	// events are generated recursively, so the deepest level of prerequisite appears first
   182  
   183  	e0 := eventSink.events[0]
   184  	assert.Equal(t, f1.Key, e0.TargetFlagKey)
   185  	assert.Equal(t, flagTestContext, e0.Context)
   186  	assert.Equal(t, &f2, e0.PrerequisiteFlag)
   187  	m.In(t).Assert(e0.PrerequisiteResult, ResultDetailProps(1, ldvalue.String("go"), ldreason.NewEvalReasonFallthrough()))
   188  
   189  	e1 := eventSink.events[1]
   190  	assert.Equal(t, f0.Key, e1.TargetFlagKey)
   191  	assert.Equal(t, flagTestContext, e1.Context)
   192  	assert.Equal(t, &f1, e1.PrerequisiteFlag)
   193  	m.In(t).Assert(e1.PrerequisiteResult, ResultDetailProps(1, ldvalue.String("go"), ldreason.NewEvalReasonFallthrough()))
   194  }
   195  
   196  func TestPrerequisiteCycleDetection(t *testing.T) {
   197  	for _, cycleGoesToOriginalFlag := range []bool{true, false} {
   198  		t.Run(fmt.Sprintf("cycleGoesToOriginalFlag=%t", cycleGoesToOriginalFlag), func(t *testing.T) {
   199  			f0 := ldbuilders.NewFlagBuilder("feature0").
   200  				On(true).
   201  				OffVariation(1).
   202  				AddPrerequisite("feature1", 1).
   203  				FallthroughVariation(0).
   204  				Variations(fallthroughValue, offValue, onValue).
   205  				Build()
   206  			f1 := ldbuilders.NewFlagBuilder("feature1").
   207  				On(true).
   208  				AddPrerequisite("feature2", 1).
   209  				FallthroughVariation(1). // this 1 matches the 1 in f0's prerequisites
   210  				Variations(ldvalue.String("nogo"), ldvalue.String("go")).
   211  				Build()
   212  			cycleTargetKey := f1.Key
   213  			if cycleGoesToOriginalFlag {
   214  				cycleTargetKey = f0.Key
   215  			}
   216  			f2 := ldbuilders.NewFlagBuilder("feature2").
   217  				On(true).
   218  				AddPrerequisite(cycleTargetKey, 1). // deliberate error
   219  				FallthroughVariation(1).
   220  				Variations(ldvalue.String("nogo"), ldvalue.String("go")).
   221  				Build()
   222  
   223  			logCapture := ldlogtest.NewMockLog()
   224  			evaluator := NewEvaluatorWithOptions(
   225  				basicDataProvider().withStoredFlags(f0, f1, f2),
   226  				EvaluatorOptionErrorLogger(logCapture.Loggers.ForLevel(ldlog.Error)),
   227  			)
   228  
   229  			result := evaluator.Evaluate(&f0, flagTestContext, FailOnAnyPrereqEvent(t))
   230  			m.In(t).Assert(result, ResultDetailError(ldreason.EvalErrorMalformedFlag))
   231  
   232  			// Note, we used FailOnAnyPrereqEvent because we would only generate a prerequisite event after
   233  			// we *finish* evaluating a prerequisite-- and due to the cycle, none of these evaluations can
   234  			// ever successfully finish.
   235  
   236  			errorLines := logCapture.GetOutput(ldlog.Error)
   237  			require.Len(t, errorLines, 1)
   238  			assert.Regexp(t, `Invalid flag configuration.*prerequisite relationship.*circular reference`, errorLines[0])
   239  		})
   240  	}
   241  }
   242  

View as plain text