...

Source file src/github.com/launchdarkly/go-server-sdk-evaluation/v2/evaluator_big_segment_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-sdk-common/v3/ldattr"
     8  	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
     9  	"github.com/launchdarkly/go-sdk-common/v3/ldreason"
    10  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    11  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
    12  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  )
    16  
    17  const basicUserKey = "userkey"
    18  
    19  type simpleBigSegmentProvider struct {
    20  	getMembership         func(contextKey string) BigSegmentMembership
    21  	getStatus             func(contextKey string) ldreason.BigSegmentsStatus
    22  	membershipKeysQueried []string
    23  }
    24  
    25  func basicBigSegmentsProvider() *simpleBigSegmentProvider {
    26  	return &simpleBigSegmentProvider{}
    27  }
    28  
    29  func (s *simpleBigSegmentProvider) GetMembership(contextKey string) (BigSegmentMembership, ldreason.BigSegmentsStatus) {
    30  	s.membershipKeysQueried = append(s.membershipKeysQueried, contextKey)
    31  	var membership BigSegmentMembership
    32  	if s.getMembership != nil {
    33  		membership = s.getMembership(contextKey)
    34  	}
    35  	status := ldreason.BigSegmentsHealthy
    36  	if s.getStatus != nil {
    37  		status = s.getStatus(contextKey)
    38  	}
    39  	return membership, status
    40  }
    41  
    42  func (s *simpleBigSegmentProvider) withStatus(status ldreason.BigSegmentsStatus) *simpleBigSegmentProvider {
    43  	return &simpleBigSegmentProvider{
    44  		getStatus:     func(string) ldreason.BigSegmentsStatus { return status },
    45  		getMembership: s.getMembership,
    46  	}
    47  }
    48  
    49  func (s *simpleBigSegmentProvider) withStatusForKey(key string, status ldreason.BigSegmentsStatus) *simpleBigSegmentProvider {
    50  	previousGetStatus := s.getStatus
    51  	return &simpleBigSegmentProvider{
    52  		getStatus: func(queriedKey string) ldreason.BigSegmentsStatus {
    53  			if key == queriedKey {
    54  				return status
    55  			}
    56  			if previousGetStatus != nil {
    57  				return previousGetStatus(queriedKey)
    58  			}
    59  			return ldreason.BigSegmentsHealthy
    60  		},
    61  		getMembership: s.getMembership,
    62  	}
    63  }
    64  
    65  func (s *simpleBigSegmentProvider) withMembership(
    66  	key string,
    67  	membership *simpleMembership,
    68  ) *simpleBigSegmentProvider {
    69  	previousGetMembership := s.getMembership
    70  	return &simpleBigSegmentProvider{
    71  		getStatus: s.getStatus,
    72  		getMembership: func(queriedKey string) BigSegmentMembership {
    73  			if key == queriedKey {
    74  				return membership
    75  			}
    76  			if previousGetMembership != nil {
    77  				return previousGetMembership(queriedKey)
    78  			}
    79  			return nil
    80  		},
    81  	}
    82  }
    83  
    84  type simpleMembership struct {
    85  	segmentChecks []string
    86  	included      []string
    87  	excluded      []string
    88  }
    89  
    90  func (m *simpleMembership) CheckMembership(segmentRef string) ldvalue.OptionalBool {
    91  	m.segmentChecks = append(m.segmentChecks, segmentRef)
    92  	for _, inc := range m.included {
    93  		if inc == segmentRef {
    94  			return ldvalue.NewOptionalBool(true)
    95  		}
    96  	}
    97  	for _, exc := range m.excluded {
    98  		if exc == segmentRef {
    99  			return ldvalue.NewOptionalBool(false)
   100  		}
   101  	}
   102  	return ldvalue.OptionalBool{}
   103  }
   104  
   105  func basicMembership() *simpleMembership { return &simpleMembership{} }
   106  
   107  func (s *simpleMembership) include(segmentRefs ...string) *simpleMembership {
   108  	s.included = append(s.included, segmentRefs...)
   109  	return s
   110  }
   111  
   112  func (s *simpleMembership) exclude(segmentRefs ...string) *simpleMembership {
   113  	s.excluded = append(s.excluded, segmentRefs...)
   114  	return s
   115  }
   116  
   117  func TestBigSegmentWithNoProviderIsNotMatched(t *testing.T) {
   118  	evaluator := NewEvaluator(
   119  		basicDataProvider().withStoredSegments(
   120  			ldbuilders.NewSegmentBuilder("segmentkey").
   121  				Unbounded(true).
   122  				Generation(1).
   123  				Included(basicUserKey). // Included should be ignored for a big segment
   124  				Build(),
   125  		),
   126  	)
   127  	f := makeBooleanFlagToMatchAnyOfSegments("segmentkey")
   128  
   129  	result := evaluator.Evaluate(&f, ldcontext.New(basicUserKey), nil)
   130  	assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
   131  	assert.Equal(t, ldreason.BigSegmentsNotConfigured, result.Detail.Reason.GetBigSegmentsStatus())
   132  }
   133  
   134  func TestBigSegmentWithNoGenerationIsNotMatched(t *testing.T) {
   135  	segment := ldbuilders.NewSegmentBuilder("segmentkey").
   136  		Unbounded(true). // but we didn't set Generation
   137  		Build()
   138  	evaluator := NewEvaluatorWithOptions(
   139  		basicDataProvider().withStoredSegments(segment),
   140  		EvaluatorOptionBigSegmentProvider(basicBigSegmentsProvider().withMembership(basicUserKey,
   141  			basicMembership().include(makeBigSegmentRef(&segment)))),
   142  	)
   143  	f := makeBooleanFlagToMatchAnyOfSegments(segment.Key)
   144  
   145  	result := evaluator.Evaluate(&f, ldcontext.New(basicUserKey), nil)
   146  	assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
   147  	assert.Equal(t, ldreason.BigSegmentsNotConfigured, result.Detail.Reason.GetBigSegmentsStatus())
   148  }
   149  
   150  func TestBigSegmentMatch(t *testing.T) {
   151  	contextKey := "contextkey"
   152  	segmentKey := "segmentkey"
   153  	flag := makeBooleanFlagToMatchAnyOfSegments(segmentKey)
   154  	makeEvaluator := func(segment ldmodel.Segment, contextMembership *simpleMembership) Evaluator {
   155  		return NewEvaluatorWithOptions(
   156  			basicDataProvider().withStoredSegments(segment),
   157  			EvaluatorOptionBigSegmentProvider(basicBigSegmentsProvider().withMembership(contextKey, contextMembership)),
   158  		)
   159  	}
   160  	for _, contextKind := range []ldcontext.Kind{"", ldcontext.DefaultKind, "other"} {
   161  		for _, isMultiKind := range []bool{false, true} {
   162  			t.Run(fmt.Sprintf("kind=%s, isMultiKind=%t", contextKind, isMultiKind), func(t *testing.T) {
   163  				context := ldcontext.NewWithKind(contextKind, contextKey)
   164  				if isMultiKind {
   165  					context = ldcontext.NewMulti(context, ldcontext.NewWithKind("irrelevantKind", "irrelevantKey"))
   166  				}
   167  				contextWithoutDesiredKind := ldcontext.NewWithKind("irrelevantKind", contextKey)
   168  				if isMultiKind {
   169  					contextWithoutDesiredKind = ldcontext.NewMulti(contextWithoutDesiredKind,
   170  						ldcontext.NewWithKind("irrelevantKind2", "irrelevantKey"))
   171  				}
   172  
   173  				t.Run("includes", func(t *testing.T) {
   174  					segment := ldbuilders.NewSegmentBuilder(segmentKey).
   175  						Unbounded(true).
   176  						UnboundedContextKind(contextKind).
   177  						Generation(2).
   178  						Build()
   179  					evaluator := makeEvaluator(segment, basicMembership().include(makeBigSegmentRef(&segment)))
   180  
   181  					t.Run("matched by include", func(t *testing.T) {
   182  						result := evaluator.Evaluate(&flag, context, nil)
   183  						assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   184  						assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
   185  					})
   186  
   187  					t.Run("unmatched if context does not have specified kind", func(t *testing.T) {
   188  						result := evaluator.Evaluate(&flag, contextWithoutDesiredKind, nil)
   189  						assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
   190  						// BigSegmentsStatus should *not* have been set because we don't even do the big segment store
   191  						// query if the context doesn't have the right kind.
   192  						assert.Equal(t, ldreason.BigSegmentsStatus(""), result.Detail.Reason.GetBigSegmentsStatus())
   193  					})
   194  				})
   195  
   196  				t.Run("rule match", func(t *testing.T) {
   197  					segment := ldbuilders.NewSegmentBuilder(segmentKey).
   198  						Unbounded(true).
   199  						UnboundedContextKind(contextKind).
   200  						Generation(2).
   201  						AddRule(
   202  							ldbuilders.NewSegmentRuleBuilder().Clauses(makeClauseToMatchAnyContextOfKind(contextKind)),
   203  						).
   204  						Build()
   205  					evaluator := makeEvaluator(segment, basicMembership())
   206  
   207  					t.Run("matched by rule", func(t *testing.T) {
   208  						result := evaluator.Evaluate(&flag, context, nil)
   209  						assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   210  						assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
   211  					})
   212  
   213  					t.Run("rules ignored if context does not have specified kind", func(t *testing.T) {
   214  						result := evaluator.Evaluate(&flag, contextWithoutDesiredKind, nil)
   215  						assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
   216  						// BigSegmentsStatus should *not* have been set because we don't even do the big segment store
   217  						// query if the context doesn't have the right kind.
   218  						assert.Equal(t, ldreason.BigSegmentsStatus(""), result.Detail.Reason.GetBigSegmentsStatus())
   219  					})
   220  
   221  					t.Run("exclude takes priority over rule", func(t *testing.T) {
   222  						evaluatorWithExclude := makeEvaluator(segment,
   223  							basicMembership().exclude(makeBigSegmentRef(&segment)))
   224  						result := evaluatorWithExclude.Evaluate(&flag, context, nil)
   225  						assert.Equal(t, ldvalue.Bool(false), result.Detail.Value)
   226  						assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
   227  					})
   228  				})
   229  			})
   230  		}
   231  	}
   232  }
   233  
   234  func TestBigSegmentIsMatchedWithRuleWhenSegmentDataForUserShowsNoMatch(t *testing.T) {
   235  	segment := ldbuilders.NewSegmentBuilder("segmentkey").
   236  		Unbounded(true).
   237  		Generation(2).
   238  		AddRule(ldbuilders.NewSegmentRuleBuilder().
   239  			Clauses(ldbuilders.Clause(ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(basicUserKey)))).
   240  		Build()
   241  	evaluator := NewEvaluatorWithOptions(
   242  		basicDataProvider().withStoredSegments(segment),
   243  		EvaluatorOptionBigSegmentProvider(
   244  			basicBigSegmentsProvider().withMembership(basicUserKey, basicMembership())),
   245  	)
   246  	f := makeBooleanFlagToMatchAnyOfSegments(segment.Key)
   247  
   248  	result := evaluator.Evaluate(&f, ldcontext.New(basicUserKey), nil)
   249  	assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   250  	assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
   251  }
   252  
   253  func TestBigSegmentStatusIsReturnedFromProvider(t *testing.T) {
   254  	segment := ldbuilders.NewSegmentBuilder("segmentkey").
   255  		Unbounded(true).
   256  		Generation(2).
   257  		Build()
   258  	evaluator := NewEvaluatorWithOptions(
   259  		basicDataProvider().withStoredSegments(segment),
   260  		EvaluatorOptionBigSegmentProvider(basicBigSegmentsProvider().
   261  			withMembership(basicUserKey, basicMembership().include(makeBigSegmentRef(&segment))).
   262  			withStatus(ldreason.BigSegmentsStale)),
   263  	)
   264  	f := makeBooleanFlagToMatchAnyOfSegments(segment.Key)
   265  
   266  	result := evaluator.Evaluate(&f, ldcontext.New(basicUserKey), nil)
   267  	assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   268  	assert.Equal(t, ldreason.BigSegmentsStale, result.Detail.Reason.GetBigSegmentsStatus())
   269  }
   270  
   271  func TestBigSegmentStateIsQueriedOnlyOncePerUniqueContextKey(t *testing.T) {
   272  	segmentKey1, segmentKey2 := "segmentKey1", "segmentKey2"
   273  	flag := makeBooleanFlagToMatchAllOfSegments(segmentKey1, segmentKey2)
   274  	// Note: in this flag configuration, both segment keys are referenced in one clause, so if
   275  	// segmentKey1 is a match then we should still also see it testing segmentKey2.
   276  
   277  	t.Run("single context kind", func(t *testing.T) {
   278  		contextKey := "contextKey"
   279  		context := ldcontext.New(contextKey)
   280  		segment1 := ldbuilders.NewSegmentBuilder(segmentKey1).Unbounded(true).Generation(1).Build()
   281  		segment2 := ldbuilders.NewSegmentBuilder(segmentKey2).Unbounded(true).Generation(2).Build()
   282  		membership := basicMembership().include(makeBigSegmentRef(&segment1), makeBigSegmentRef(&segment2))
   283  		bigSegmentsProvider := basicBigSegmentsProvider().withMembership(contextKey, membership)
   284  		evaluator := NewEvaluatorWithOptions(
   285  			basicDataProvider().withStoredSegments(segment1, segment2),
   286  			EvaluatorOptionBigSegmentProvider(bigSegmentsProvider),
   287  		)
   288  
   289  		result := evaluator.Evaluate(&flag, context, nil)
   290  
   291  		assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   292  		assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
   293  		assert.Equal(t, []string{contextKey}, bigSegmentsProvider.membershipKeysQueried)
   294  		assert.Equal(t, []string{makeBigSegmentRef(&segment1), makeBigSegmentRef(&segment2)}, membership.segmentChecks)
   295  	})
   296  
   297  	t.Run("two context kinds referenced, both have same key", func(t *testing.T) {
   298  		contextKey := "contextKey"
   299  		otherKind := ldcontext.Kind("other")
   300  		context := ldcontext.NewMulti(
   301  			ldcontext.New(contextKey), // default context kind
   302  			ldcontext.NewWithKind(otherKind, contextKey),
   303  		)
   304  		segment1 := ldbuilders.NewSegmentBuilder(segmentKey1).Unbounded(true).Generation(1).Build()
   305  		segment2 := ldbuilders.NewSegmentBuilder(segmentKey2).Unbounded(true).Generation(2).
   306  			UnboundedContextKind(otherKind).Build()
   307  		membership := basicMembership().include(makeBigSegmentRef(&segment1), makeBigSegmentRef(&segment2))
   308  		bigSegmentsProvider := basicBigSegmentsProvider().withMembership(contextKey, membership)
   309  		evaluator := NewEvaluatorWithOptions(
   310  			basicDataProvider().withStoredSegments(segment1, segment2),
   311  			EvaluatorOptionBigSegmentProvider(bigSegmentsProvider),
   312  		)
   313  
   314  		result := evaluator.Evaluate(&flag, context, nil)
   315  
   316  		assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   317  		assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
   318  		assert.Equal(t, []string{contextKey}, bigSegmentsProvider.membershipKeysQueried)
   319  		assert.Equal(t, []string{makeBigSegmentRef(&segment1), makeBigSegmentRef(&segment2)}, membership.segmentChecks)
   320  	})
   321  
   322  	t.Run("two context kinds referenced, each with a different key", func(t *testing.T) {
   323  		contextKey1, contextKey2 := "contextKey1", "contextKey2"
   324  		otherKind := ldcontext.Kind("other")
   325  		context := ldcontext.NewMulti(
   326  			ldcontext.New(contextKey1), // default context kind
   327  			ldcontext.NewWithKind(otherKind, contextKey2),
   328  		)
   329  		segment1 := ldbuilders.NewSegmentBuilder(segmentKey1).Unbounded(true).Generation(1).Build()
   330  		segment2 := ldbuilders.NewSegmentBuilder(segmentKey2).Unbounded(true).Generation(2).
   331  			UnboundedContextKind(otherKind).Build()
   332  		membershipForKey1 := basicMembership().include(makeBigSegmentRef(&segment1))
   333  		membershipForKey2 := basicMembership().include(makeBigSegmentRef(&segment2))
   334  		bigSegmentsProvider := basicBigSegmentsProvider().
   335  			withMembership(contextKey1, membershipForKey1).
   336  			withMembership(contextKey2, membershipForKey2)
   337  		evaluator := NewEvaluatorWithOptions(
   338  			basicDataProvider().withStoredSegments(segment1, segment2),
   339  			EvaluatorOptionBigSegmentProvider(bigSegmentsProvider),
   340  		)
   341  
   342  		result := evaluator.Evaluate(&flag, context, nil)
   343  
   344  		assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   345  		assert.Equal(t, ldreason.BigSegmentsHealthy, result.Detail.Reason.GetBigSegmentsStatus())
   346  		assert.Equal(t, []string{contextKey1, contextKey2}, bigSegmentsProvider.membershipKeysQueried)
   347  		assert.Equal(t, []string{makeBigSegmentRef(&segment1)}, membershipForKey1.segmentChecks)
   348  		assert.Equal(t, []string{makeBigSegmentRef(&segment2)}, membershipForKey2.segmentChecks)
   349  	})
   350  }
   351  
   352  func TestBigSegmentStatusWithMultipleQueries(t *testing.T) {
   353  	// A single evaluation could end up doing more than one big segments query if there are two different
   354  	// context keys involved. If those queries don't return the same status, we want to make sure we
   355  	// report whichever status is most problematic: StoreError is the worst, Stale is the second worst.
   356  	segmentKey1, segmentKey2 := "segmentKey1", "segmentKey2"
   357  	contextKey1, contextKey2 := "contextKey1", "contextKey2"
   358  	otherKind := ldcontext.Kind("other")
   359  	context := ldcontext.NewMulti(
   360  		ldcontext.New(contextKey1), // default context kind
   361  		ldcontext.NewWithKind(otherKind, contextKey2),
   362  	)
   363  	segment1 := ldbuilders.NewSegmentBuilder(segmentKey1).Unbounded(true).Generation(1).Build()
   364  	segment2 := ldbuilders.NewSegmentBuilder(segmentKey2).Unbounded(true).Generation(2).
   365  		UnboundedContextKind(otherKind).Build()
   366  	membershipForKey1 := basicMembership().include(makeBigSegmentRef(&segment1))
   367  	membershipForKey2 := basicMembership().include(makeBigSegmentRef(&segment2))
   368  	flag := makeBooleanFlagToMatchAllOfSegments(segmentKey1, segmentKey2)
   369  
   370  	type params struct{ status1, status2, expected ldreason.BigSegmentsStatus }
   371  	var allParams []params
   372  	allStatuses := []ldreason.BigSegmentsStatus{ldreason.BigSegmentsHealthy, ldreason.BigSegmentsStale,
   373  		ldreason.BigSegmentsStoreError, ldreason.BigSegmentsNotConfigured}
   374  	for i := 0; i < len(allStatuses)-1; i++ {
   375  		better, worse := allStatuses[i], allStatuses[i+1]
   376  		allParams = append(allParams, params{better, worse, worse})
   377  		allParams = append(allParams, params{worse, better, worse})
   378  	}
   379  	for _, p := range allParams {
   380  		t.Run(fmt.Sprintf("%s, %s", p.status1, p.status2), func(t *testing.T) {
   381  			bigSegmentsProvider := basicBigSegmentsProvider().
   382  				withMembership(contextKey1, membershipForKey1).
   383  				withMembership(contextKey2, membershipForKey2).
   384  				withStatusForKey(contextKey1, p.status1).
   385  				withStatusForKey(contextKey2, p.status2)
   386  			evaluator := NewEvaluatorWithOptions(
   387  				basicDataProvider().withStoredSegments(segment1, segment2),
   388  				EvaluatorOptionBigSegmentProvider(bigSegmentsProvider),
   389  			)
   390  
   391  			result := evaluator.Evaluate(&flag, context, nil)
   392  
   393  			assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   394  			assert.Equal(t, []string{contextKey1, contextKey2}, bigSegmentsProvider.membershipKeysQueried)
   395  			assert.Equal(t, p.expected, result.Detail.Reason.GetBigSegmentsStatus())
   396  		})
   397  	}
   398  }
   399  
   400  func TestBigSegmentStatusIsReturnedWhenBigSegmentWasReferencedFromPrerequisiteFlag(t *testing.T) {
   401  	segment := ldbuilders.NewSegmentBuilder("segmentkey").
   402  		Unbounded(true).
   403  		Generation(2).
   404  		Build()
   405  
   406  	f1 := makeBooleanFlagToMatchAnyOfSegments(segment.Key)
   407  	f0 := ldbuilders.NewFlagBuilder("feature0").
   408  		On(true).
   409  		Variations(ldvalue.Bool(false), ldvalue.Bool(true)).FallthroughVariation(1).
   410  		AddPrerequisite(f1.Key, 1).
   411  		Build()
   412  
   413  	evaluator := NewEvaluatorWithOptions(
   414  		basicDataProvider().withStoredFlags(f1).withStoredSegments(segment),
   415  		EvaluatorOptionBigSegmentProvider(basicBigSegmentsProvider().
   416  			withMembership(basicUserKey, basicMembership().include(makeBigSegmentRef(&segment))).
   417  			withStatus(ldreason.BigSegmentsStale)),
   418  	)
   419  
   420  	result := evaluator.Evaluate(&f0, ldcontext.New(basicUserKey), nil)
   421  	assert.Equal(t, ldvalue.Bool(true), result.Detail.Value)
   422  	assert.Equal(t, ldreason.BigSegmentsStale, result.Detail.Reason.GetBigSegmentsStatus())
   423  }
   424  

View as plain text