...

Source file src/github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldtestdata/test_data_source.go

Documentation: github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldtestdata

     1  package ldtestdata
     2  
     3  import (
     4  	"sync"
     5  
     6  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
     7  	"github.com/launchdarkly/go-server-sdk/v6/interfaces"
     8  	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
     9  	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoreimpl"
    10  	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes"
    11  
    12  	"golang.org/x/exp/slices"
    13  )
    14  
    15  // TestDataSource is a test fixture that provides dynamically updatable feature flag state in a
    16  // simplified form to an SDK client in test scenarios.
    17  //
    18  // See package description for more details and usage examples.
    19  type TestDataSource struct {
    20  	currentFlags    map[string]ldstoretypes.ItemDescriptor
    21  	currentBuilders map[string]*FlagBuilder
    22  	currentSegments map[string]ldstoretypes.ItemDescriptor
    23  	instances       []*testDataSourceImpl
    24  	lock            sync.Mutex
    25  }
    26  
    27  type testDataSourceImpl struct {
    28  	owner   *TestDataSource
    29  	updates subsystems.DataSourceUpdateSink
    30  }
    31  
    32  // DataSource creates an instance of [TestDataSource].
    33  //
    34  // Storing this object in the DataSource field of [github.com/launchdarkly/go-server-sdk/v6.Config]
    35  // causes the SDK client to use the test data. Any subsequent changes made using methods like
    36  // [TestDataSource.Update] will propagate to all LDClient instances that are using this data source.
    37  func DataSource() *TestDataSource {
    38  	return &TestDataSource{
    39  		currentFlags:    make(map[string]ldstoretypes.ItemDescriptor),
    40  		currentBuilders: make(map[string]*FlagBuilder),
    41  		currentSegments: make(map[string]ldstoretypes.ItemDescriptor),
    42  	}
    43  }
    44  
    45  // Flag creates or copies a [FlagBuilder] for building a test flag configuration.
    46  //
    47  // If this flag key has already been defined in this TestDataSource instance, then the builder
    48  // starts with the same configuration that was last provided for this flag.
    49  //
    50  // Otherwise, it starts with a new default configuration in which the flag has true and false
    51  // variations, is true for all users when targeting is turned on and false otherwise, and
    52  // currently has targeting turned on. You can change any of those properties, and provide more
    53  // complex behavior, using the FlagBuilder methods.
    54  //
    55  // Once you have set the desired configuration, pass the builder to Update.
    56  func (t *TestDataSource) Flag(key string) *FlagBuilder {
    57  	t.lock.Lock()
    58  	defer t.lock.Unlock()
    59  	existingBuilder := t.currentBuilders[key]
    60  	if existingBuilder == nil {
    61  		return newFlagBuilder(key).BooleanFlag()
    62  	}
    63  	return copyFlagBuilder(existingBuilder)
    64  }
    65  
    66  // Update updates the test data with the specified flag configuration.
    67  //
    68  // This has the same effect as if a flag were added or modified on the LaunchDarkly dashboard.
    69  // It immediately propagates the flag change to any LDClient instance(s) that you have already
    70  // configured to use this TestDataSource. If no LDClient has been started yet, it simply adds
    71  // this flag to the test data which will be provided to any LDClient that you subsequently
    72  // configure.
    73  //
    74  // Any subsequent changes to this FlagBuilder instance do not affect the test data, unless
    75  // you call Update again.
    76  func (t *TestDataSource) Update(flagBuilder *FlagBuilder) *TestDataSource {
    77  	key := flagBuilder.key
    78  	clonedBuilder := copyFlagBuilder(flagBuilder)
    79  	t.updateInternal(key, flagBuilder.createFlag, clonedBuilder)
    80  	return t
    81  }
    82  
    83  // UpdateStatus simulates a change in the data source status.
    84  //
    85  // Use this if you want to test the behavior of application code that uses
    86  // LDClient.GetDataSourceStatusProvider to track whether the data source is having problems (for example,
    87  // a network failure interruptsingthe streaming connection). It does not actually stop the
    88  // TestDataSource from working, so even if you have simulated an outage, calling Update will still send
    89  // updates.
    90  func (t *TestDataSource) UpdateStatus(
    91  	newState interfaces.DataSourceState,
    92  	newError interfaces.DataSourceErrorInfo,
    93  ) *TestDataSource {
    94  	t.lock.Lock()
    95  	instances := slices.Clone(t.instances)
    96  	t.lock.Unlock()
    97  
    98  	for _, instance := range instances {
    99  		instance.updates.UpdateStatus(newState, newError)
   100  	}
   101  
   102  	return t
   103  }
   104  
   105  // UsePreconfiguredFlag copies a full feature flag data model object into the test data.
   106  //
   107  // It immediately propagates the flag change to any LDClient instance(s) that you have already
   108  // configured to use this TestDataSource. If no LDClient has been started yet, it simply adds
   109  // this flag to the test data which will be provided to any LDClient that you subsequently
   110  // configure.
   111  //
   112  // Use this method if you need to use advanced flag configuration properties that are not supported by
   113  // the simplified FlagBuilder API. Otherwise it is recommended to use the regular Flag/Update
   114  // mechanism to avoid dependencies on details of the data model.
   115  //
   116  // You cannot make incremental changes with Flag/Update to a flag that has been added in this way;
   117  // you can only replace it with an entirely new flag configuration.
   118  //
   119  // To construct an instance of ldmodel.FeatureFlag, rather than accessing the fields directly it is
   120  // recommended to use the builder API in [github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders].
   121  func (t *TestDataSource) UsePreconfiguredFlag(flag ldmodel.FeatureFlag) *TestDataSource {
   122  	t.updateInternal(
   123  		flag.Key,
   124  		func(version int) ldmodel.FeatureFlag {
   125  			f := flag
   126  			if f.Version < version {
   127  				f.Version = version
   128  			}
   129  			return f
   130  		},
   131  		nil,
   132  	)
   133  	return t
   134  }
   135  
   136  // UsePreconfiguredSegment copies a full user segment data model object into the test data.
   137  //
   138  // It immediately propagates the flag change to any LDClient instance(s) that you have already
   139  // configured to use this TestDataSource. If no LDClient has been started yet, it simply adds
   140  // this flag to the test data which will be provided to any LDClient that you subsequently
   141  // configure.
   142  //
   143  // This method is currently the only way to inject user segment data, since there is no builder
   144  // API for segments. It is mainly intended for the SDK's own tests of user segment functionality,
   145  // since application tests that need to produce a desired evaluation state could do so more easily
   146  // by just setting flag values.
   147  //
   148  // To construct an instance of ldmodel.Segment, rather than accessing the fields directly it is
   149  // recommended to use the builder API in [github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders].
   150  func (t *TestDataSource) UsePreconfiguredSegment(segment ldmodel.Segment) *TestDataSource {
   151  	t.lock.Lock()
   152  	oldItem := t.currentSegments[segment.Key]
   153  	newSegment := segment
   154  	newSegment.Version = oldItem.Version + 1
   155  	newItem := ldstoretypes.ItemDescriptor{Version: newSegment.Version, Item: &newSegment}
   156  	t.currentSegments[segment.Key] = newItem
   157  	instances := slices.Clone(t.instances)
   158  	t.lock.Unlock()
   159  
   160  	for _, instance := range instances {
   161  		instance.updates.Upsert(ldstoreimpl.Segments(), segment.Key, newItem)
   162  	}
   163  
   164  	return t
   165  }
   166  
   167  func (t *TestDataSource) updateInternal(
   168  	key string,
   169  	makeFlag func(int) ldmodel.FeatureFlag,
   170  	builder *FlagBuilder,
   171  ) {
   172  	t.lock.Lock()
   173  	oldItem := t.currentFlags[key]
   174  	newVersion := oldItem.Version + 1
   175  	newFlag := makeFlag(newVersion)
   176  	newItem := ldstoretypes.ItemDescriptor{Version: newVersion, Item: &newFlag}
   177  	t.currentFlags[key] = newItem
   178  	t.currentBuilders[key] = builder
   179  	instances := slices.Clone(t.instances)
   180  	t.lock.Unlock()
   181  
   182  	for _, instance := range instances {
   183  		instance.updates.Upsert(ldstoreimpl.Features(), key, newItem)
   184  	}
   185  }
   186  
   187  // Build is called internally by the SDK to associate this test data source with an
   188  // LDClient instance. You do not need to call this method.
   189  func (t *TestDataSource) Build(context subsystems.ClientContext) (subsystems.DataSource, error) {
   190  	instance := &testDataSourceImpl{owner: t, updates: context.GetDataSourceUpdateSink()}
   191  	t.lock.Lock()
   192  	t.instances = append(t.instances, instance)
   193  	t.lock.Unlock()
   194  	return instance, nil
   195  }
   196  
   197  func (t *TestDataSource) makeInitData() []ldstoretypes.Collection {
   198  	t.lock.Lock()
   199  	defer t.lock.Unlock()
   200  	flags := make([]ldstoretypes.KeyedItemDescriptor, 0, len(t.currentFlags))
   201  	segments := make([]ldstoretypes.KeyedItemDescriptor, 0, len(t.currentSegments))
   202  	for key, item := range t.currentFlags {
   203  		flags = append(flags, ldstoretypes.KeyedItemDescriptor{Key: key, Item: item})
   204  	}
   205  	for key, item := range t.currentSegments {
   206  		segments = append(segments, ldstoretypes.KeyedItemDescriptor{Key: key, Item: item})
   207  	}
   208  	return []ldstoretypes.Collection{
   209  		{Kind: ldstoreimpl.Features(), Items: flags},
   210  		{Kind: ldstoreimpl.Segments(), Items: segments},
   211  	}
   212  }
   213  
   214  func (t *TestDataSource) closedInstance(instance *testDataSourceImpl) {
   215  	t.lock.Lock()
   216  	defer t.lock.Unlock()
   217  	for i, in := range t.instances {
   218  		if in == instance {
   219  			copy(t.instances[i:], t.instances[i+1:])
   220  			t.instances[len(t.instances)-1] = nil
   221  			t.instances = t.instances[:len(t.instances)-1]
   222  			break
   223  		}
   224  	}
   225  }
   226  
   227  func (d *testDataSourceImpl) Close() error {
   228  	d.owner.closedInstance(d)
   229  	return nil
   230  }
   231  
   232  func (d *testDataSourceImpl) IsInitialized() bool {
   233  	return true
   234  }
   235  
   236  func (d *testDataSourceImpl) Start(closeWhenReady chan<- struct{}) {
   237  	_ = d.updates.Init(d.owner.makeInitData())
   238  	d.updates.UpdateStatus(interfaces.DataSourceStateValid, interfaces.DataSourceErrorInfo{})
   239  	close(closeWhenReady)
   240  }
   241  

View as plain text