Skip to content

Commit 4d8e9a6

Browse files
chore: updates contract tests CI to also test FDv2 cases (#195)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Enables FDv2 contract tests in CI and adds DataSystem initializer/synchronizer config support to the contract test service. > > - **CI** > - Update `.github/actions/contract-tests/action.yml` to accept `run_fdv2_tests` input and, when true, set up Go, launch service, clone `sdk-test-harness` `feat/fdv2`, and run FDv2 tests. > - Pass `run_fdv2_tests: 'true'` in `.github/workflows/sdk-server-ci.yml`. > - **Contract Test Service** > - Extend `Representations.cs` with `SdkConfigDataSystemParams` model (store, mode, initializers, synchronizers, polling/streaming params, constants). > - In `SdkClientEntity.cs`, wire up DataSystem initializers and synchronizers via `Components.DataSystem()`; add required usings. (Persistent store wiring left commented out.) > - **Tests** > - Add `pkgs/sdk/server/contract-tests/test-supressions-fdv2.txt` for FDv2-specific skips. > - **Assembly** > - Grant internals access to `ContractTestService` in `AssemblyInfo.cs`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 23b91ec. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Ryan Lamb <[email protected]>
1 parent 0ad80c9 commit 4d8e9a6

File tree

6 files changed

+358
-0
lines changed

6 files changed

+358
-0
lines changed

.github/actions/contract-tests/action.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ inputs:
1111
description: 'Github token, used for contract tests'
1212
required: false
1313
default: ''
14+
run_fdv2_tests:
15+
description: 'Whether to run contract tests from the feat/fdv2 branch'
16+
required: false
17+
default: 'false'
1418

1519
runs:
1620
using: composite
@@ -47,3 +51,29 @@ runs:
4751
test_service_port: 8000
4852
extra_params: '-status-timeout=360 -skip-from=${{ env.SUPPRESSION_FILE }}'
4953
token: ${{ inputs.token }}
54+
55+
- name: Setup Go
56+
if: inputs.run_fdv2_tests == 'true'
57+
uses: actions/setup-go@v5
58+
with:
59+
go-version: '1.21'
60+
61+
- name: Launch Contract Tests FDv2 Flavor
62+
if: inputs.run_fdv2_tests == 'true'
63+
id: launch-contract-tests-fdv2
64+
shell: bash
65+
run: dotnet ${{ inputs.service_dll_file }} > test-service.log 2>&1 & disown
66+
67+
- name: Clone and run contract tests from feat/fdv2 branch
68+
if: inputs.run_fdv2_tests == 'true'
69+
shell: bash
70+
run: |
71+
mkdir -p /tmp/sdk-test-harness
72+
git clone https://github.com/launchdarkly/sdk-test-harness.git /tmp/sdk-test-harness
73+
cp $(dirname ./${{ inputs.service_project_file }})/test-supressions-fdv2.txt /tmp/sdk-test-harness/testharness-suppressions-fdv2.txt
74+
cd /tmp/sdk-test-harness
75+
git checkout feat/fdv2
76+
go build -o test-harness .
77+
./test-harness -url http://localhost:8000 -debug -status-timeout=360 --skip-from=testharness-suppressions-fdv2.txt --stop-service-at-end
78+
env:
79+
GITHUB_TOKEN: ${{ inputs.token }}

.github/workflows/sdk-server-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
service_project_file: ${{ env.CONTRACT_TEST_PROJECT_FILE}}
3737
service_dll_file: ${{ env.CONTRACT_TEST_DLL_FILE}}
3838
token: ${{ secrets.GITHUB_TOKEN }}
39+
run_fdv2_tests: 'true'
3940

4041
- uses: ./.github/actions/build-docs
4142
with:

pkgs/sdk/server/contract-tests/Representations.cs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class SdkConfigParams
3131
public SdkConfigBigSegmentsParams BigSegments { get; set; }
3232
public SdkTagParams Tags { get; set; }
3333
public SdkHookParams Hooks { get; set; }
34+
public SdkConfigDataSystemParams DataSystem { get; set; }
3435
}
3536

3637
public class SdkTagParams
@@ -88,6 +89,126 @@ public class SdkConfigBigSegmentsParams
8889
public long? UserCacheTimeMs { get; set; }
8990
}
9091

92+
/// <summary>
93+
/// Constants for store mode values.
94+
/// </summary>
95+
public static class StoreMode
96+
{
97+
/// <summary>
98+
/// Read-only mode - the data system will only read from the persistent store.
99+
/// </summary>
100+
public const int Read = 0;
101+
102+
/// <summary>
103+
/// Read-write mode - the data system can read from, and write to, the persistent store.
104+
/// </summary>
105+
public const int ReadWrite = 1;
106+
}
107+
108+
/// <summary>
109+
/// Constants for persistent store type values.
110+
/// </summary>
111+
public static class PersistentStoreType
112+
{
113+
/// <summary>
114+
/// Redis persistent store type.
115+
/// </summary>
116+
public const string Redis = "redis";
117+
118+
/// <summary>
119+
/// DynamoDB persistent store type.
120+
/// </summary>
121+
public const string DynamoDB = "dynamodb";
122+
123+
/// <summary>
124+
/// Consul persistent store type.
125+
/// </summary>
126+
public const string Consul = "consul";
127+
}
128+
129+
/// <summary>
130+
/// Constants for persistent cache mode values.
131+
/// </summary>
132+
public static class PersistentCacheMode
133+
{
134+
/// <summary>
135+
/// Cache disabled mode.
136+
/// </summary>
137+
public const string Off = "off";
138+
139+
/// <summary>
140+
/// Time-to-live cache mode with a specified TTL.
141+
/// </summary>
142+
public const string TTL = "ttl";
143+
144+
/// <summary>
145+
/// Infinite cache mode - cache forever.
146+
/// </summary>
147+
public const string Infinite = "infinite";
148+
}
149+
150+
public class SdkConfigDataSystemParams
151+
{
152+
public SdkConfigDataStoreParams Store { get; set; }
153+
public int? StoreMode { get; set; }
154+
public SdkConfigDataInitializerParams[] Initializers { get; set; }
155+
public SdkConfigSynchronizersParams Synchronizers { get; set; }
156+
public string PayloadFilter { get; set; }
157+
}
158+
159+
public class SdkConfigDataStoreParams
160+
{
161+
public SdkConfigPersistentDataStoreParams PersistentDataStore { get; set; }
162+
}
163+
164+
public class SdkConfigPersistentDataStoreParams
165+
{
166+
public SdkConfigPersistentStoreParams Store { get; set; }
167+
public SdkConfigPersistentCacheParams Cache { get; set; }
168+
}
169+
170+
public class SdkConfigPersistentStoreParams
171+
{
172+
public string Type { get; set; }
173+
public string Prefix { get; set; }
174+
public string DSN { get; set; }
175+
}
176+
177+
public class SdkConfigPersistentCacheParams
178+
{
179+
public string Mode { get; set; }
180+
public int? TTL { get; set; }
181+
}
182+
183+
public class SdkConfigDataInitializerParams
184+
{
185+
public SdkConfigPollingParams Polling { get; set; }
186+
}
187+
188+
public class SdkConfigSynchronizersParams
189+
{
190+
public SdkConfigSynchronizerParams Primary { get; set; }
191+
public SdkConfigSynchronizerParams Secondary { get; set; }
192+
}
193+
194+
public class SdkConfigSynchronizerParams
195+
{
196+
public SdkConfigStreamingParams Streaming { get; set; }
197+
public SdkConfigPollingParams Polling { get; set; }
198+
}
199+
200+
public class SdkConfigPollingParams
201+
{
202+
public Uri BaseUri { get; set; }
203+
public long? PollIntervalMs { get; set; }
204+
}
205+
206+
public class SdkConfigStreamingParams
207+
{
208+
public Uri BaseUri { get; set; }
209+
public long? InitialRetryDelayMs { get; set; }
210+
}
211+
91212
public class CommandParams
92213
{
93214
public string Command { get; set; }

pkgs/sdk/server/contract-tests/SdkClientEntity.cs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
using LaunchDarkly.Sdk.Json;
88
using LaunchDarkly.Sdk.Server;
99
using LaunchDarkly.Sdk.Server.Hooks;
10+
using LaunchDarkly.Sdk.Server.Integrations;
1011
using LaunchDarkly.Sdk.Server.Migrations;
12+
using LaunchDarkly.Sdk.Server.Subsystems;
1113

1214
namespace TestService
1315
{
@@ -372,9 +374,200 @@ private static Configuration BuildSdkConfig(SdkConfigParams sdkParams, ILogAdapt
372374
builder.Hooks(Components.Hooks(hooks));
373375
}
374376

377+
if (sdkParams.DataSystem != null)
378+
{
379+
var dataSystemBuilder = Components.DataSystem().Custom();
380+
381+
// TODO: re-enable this code in the future and determine which dependencies on persistent stores need to be added to contract test build process
382+
// Configure persistent store if provided
383+
// if (sdkParams.DataSystem.Store?.PersistentDataStore != null)
384+
// {
385+
// var storeConfig = sdkParams.DataSystem.Store.PersistentDataStore;
386+
// var storeType = storeConfig.Store.Type.ToLower();
387+
// IComponentConfigurer<IDataStore> persistentStore = null;
388+
389+
// PersistentDataStoreBuilder persistentStoreBuilder = null;
390+
391+
// switch (storeType)
392+
// {
393+
// case "redis":
394+
// var redisBuilder = Redis.DataStore();
395+
// if (!string.IsNullOrEmpty(storeConfig.Store.DSN))
396+
// {
397+
// redisBuilder.Uri(storeConfig.Store.DSN);
398+
// }
399+
// if (!string.IsNullOrEmpty(storeConfig.Store.Prefix))
400+
// {
401+
// redisBuilder.Prefix(storeConfig.Store.Prefix);
402+
// }
403+
// persistentStoreBuilder = Components.PersistentDataStore(redisBuilder);
404+
// break;
405+
406+
// case "dynamodb":
407+
// // For DynamoDB, DSN is the table name
408+
// var dynamoBuilder = DynamoDB.DataStore(storeConfig.Store.DSN ?? "sdk-contract-tests");
409+
// if (!string.IsNullOrEmpty(storeConfig.Store.Prefix))
410+
// {
411+
// dynamoBuilder.Prefix(storeConfig.Store.Prefix);
412+
// }
413+
// // DynamoDB uses IPersistentDataStoreAsync, so use the async overload
414+
// persistentStoreBuilder = Components.PersistentDataStore(dynamoBuilder);
415+
// break;
416+
417+
// case "consul":
418+
// var consulBuilder = Consul.DataStore();
419+
// if (!string.IsNullOrEmpty(storeConfig.Store.DSN))
420+
// {
421+
// consulBuilder.Address(storeConfig.Store.DSN);
422+
// }
423+
// if (!string.IsNullOrEmpty(storeConfig.Store.Prefix))
424+
// {
425+
// consulBuilder.Prefix(storeConfig.Store.Prefix);
426+
// }
427+
// persistentStoreBuilder = Components.PersistentDataStore(consulBuilder);
428+
// break;
429+
// }
430+
431+
// if (persistentStoreBuilder != null)
432+
// {
433+
// // Configure cache
434+
// var cacheMode = storeConfig.Cache?.Mode?.ToLower();
435+
// if (cacheMode == "off")
436+
// {
437+
// persistentStoreBuilder.NoCaching();
438+
// }
439+
// else if (cacheMode == "ttl" && storeConfig.Cache.TTL.HasValue)
440+
// {
441+
// persistentStoreBuilder.CacheTime(TimeSpan.FromSeconds(storeConfig.Cache.TTL.Value));
442+
// }
443+
// else if (cacheMode == "infinite")
444+
// {
445+
// persistentStoreBuilder.CacheForever();
446+
// }
447+
448+
// // Determine store mode
449+
// var storeMode = sdkParams.DataSystem.StoreMode == 0
450+
// ? DataSystemConfiguration.DataStoreMode.ReadOnly
451+
// : DataSystemConfiguration.DataStoreMode.ReadWrite;
452+
453+
// dataSystemBuilder.PersistentStore(persistentStoreBuilder, storeMode);
454+
// }
455+
// }
456+
457+
// Configure initializers
458+
if (sdkParams.DataSystem.Initializers != null && sdkParams.DataSystem.Initializers.Length > 0)
459+
{
460+
var initializers = new List<IComponentConfigurer<IDataSource>>();
461+
foreach (var initializer in sdkParams.DataSystem.Initializers)
462+
{
463+
if (initializer.Polling != null)
464+
{
465+
var pollingBuilder = DataSystemComponents.Polling();
466+
if (initializer.Polling.BaseUri != null)
467+
{
468+
var endpointOverride = Components.ServiceEndpoints().Polling(initializer.Polling.BaseUri);
469+
pollingBuilder.ServiceEndpointsOverride(endpointOverride);
470+
}
471+
if (initializer.Polling.PollIntervalMs.HasValue)
472+
{
473+
pollingBuilder.PollInterval(TimeSpan.FromMilliseconds(initializer.Polling.PollIntervalMs.Value));
474+
}
475+
if (!string.IsNullOrEmpty(sdkParams.DataSystem.PayloadFilter))
476+
{
477+
// PayloadFilter is not yet supported in FDv2 builders, so we skip it for now
478+
// TODO: Add PayloadFilter support when available
479+
}
480+
initializers.Add(pollingBuilder);
481+
}
482+
}
483+
if (initializers.Count > 0)
484+
{
485+
dataSystemBuilder.Initializers(initializers.ToArray());
486+
}
487+
}
488+
489+
// Configure synchronizers
490+
if (sdkParams.DataSystem.Synchronizers != null)
491+
{
492+
var synchronizers = new List<IComponentConfigurer<IDataSource>>();
493+
494+
// Primary synchronizer
495+
if (sdkParams.DataSystem.Synchronizers.Primary != null)
496+
{
497+
var primary = CreateSynchronizer(sdkParams.DataSystem.Synchronizers.Primary, sdkParams.DataSystem.PayloadFilter);
498+
if (primary != null)
499+
{
500+
synchronizers.Add(primary);
501+
}
502+
}
503+
504+
// Secondary synchronizer (optional)
505+
if (sdkParams.DataSystem.Synchronizers.Secondary != null)
506+
{
507+
var secondary = CreateSynchronizer(sdkParams.DataSystem.Synchronizers.Secondary, sdkParams.DataSystem.PayloadFilter);
508+
if (secondary != null)
509+
{
510+
synchronizers.Add(secondary);
511+
}
512+
}
513+
514+
if (synchronizers.Count > 0)
515+
{
516+
dataSystemBuilder.Synchronizers(synchronizers.ToArray());
517+
}
518+
}
519+
520+
builder.DataSystem(dataSystemBuilder);
521+
}
522+
375523
return builder.Build();
376524
}
377525

526+
private static IComponentConfigurer<IDataSource> CreateSynchronizer(
527+
SdkConfigSynchronizerParams synchronizer,
528+
string payloadFilter)
529+
{
530+
if (synchronizer.Polling != null)
531+
{
532+
var pollingBuilder = DataSystemComponents.Polling();
533+
if (synchronizer.Polling.BaseUri != null)
534+
{
535+
var endpointOverride = Components.ServiceEndpoints().Polling(synchronizer.Polling.BaseUri);
536+
pollingBuilder.ServiceEndpointsOverride(endpointOverride);
537+
}
538+
if (synchronizer.Polling.PollIntervalMs.HasValue)
539+
{
540+
pollingBuilder.PollInterval(TimeSpan.FromMilliseconds(synchronizer.Polling.PollIntervalMs.Value));
541+
}
542+
if (!string.IsNullOrEmpty(payloadFilter))
543+
{
544+
// PayloadFilter is not yet supported in FDv2 builders, so we skip it for now
545+
// TODO: Add PayloadFilter support when available
546+
}
547+
return pollingBuilder;
548+
}
549+
else if (synchronizer.Streaming != null)
550+
{
551+
var streamingBuilder = DataSystemComponents.Streaming();
552+
if (synchronizer.Streaming.BaseUri != null)
553+
{
554+
var endpointOverride = Components.ServiceEndpoints().Streaming(synchronizer.Streaming.BaseUri);
555+
streamingBuilder.ServiceEndpointsOverride(endpointOverride);
556+
}
557+
if (synchronizer.Streaming.InitialRetryDelayMs.HasValue)
558+
{
559+
streamingBuilder.InitialReconnectDelay(TimeSpan.FromMilliseconds(synchronizer.Streaming.InitialRetryDelayMs.Value));
560+
}
561+
if (!string.IsNullOrEmpty(payloadFilter))
562+
{
563+
// PayloadFilter is not yet supported in FDv2 builders, so we skip it for now
564+
// TODO: Add PayloadFilter support when available
565+
}
566+
return streamingBuilder;
567+
}
568+
return null;
569+
}
570+
378571
private MigrationVariationResponse DoMigrationVariation(MigrationVariationParams migrationVariation)
379572
{
380573
var defaultStage = MigrationStageExtensions.FromDataModelString(migrationVariation.DefaultStage);

0 commit comments

Comments
 (0)