-
Notifications
You must be signed in to change notification settings - Fork 23
chore: adds DataSystem and FDv2 contract test service functionality #341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cf3cba4
52122e8
bd911c0
7b4efaf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,8 @@ | ||
| TEST_HARNESS_PARAMS= -skip "events/disabling" -status-timeout 60 | ||
| # can add temporary test skips etc. here | ||
| # Currently we are skipping the "events/disabling" tests because the Android SDK has no way to | ||
| # disable events. That wasn't an issue earlier because the "events/disabling" tests were getting | ||
| # automatically skipped by sdk-test-harness for a different reason: they rely on the | ||
| # ServiceEndpoints API, which the Android SDK didn't previously support. | ||
| SUPPRESSION_FILE=testharness-suppressions.txt | ||
| SUPPRESSION_FILE_FDV2=testharness-suppressions-fdv2.txt | ||
|
|
||
| TEST_HARNESS_PARAMS_V2= -status-timeout 60 | ||
| TEST_HARNESS_PARAMS_V3= -status-timeout 60 | ||
|
|
||
| build-contract-tests: | ||
| @cd contract-tests && ../gradlew --no-daemon -s assembleDebug -PdisablePreDex | ||
|
|
@@ -14,10 +13,24 @@ start-emulator: | |
| start-contract-test-service: | ||
| @scripts/start-test-service.sh | ||
|
|
||
| # Note that only the last version of the tests have the stop-service-at-end flag set, so the contract test service will be stopped after the tests are run. | ||
| run-contract-tests: | ||
| @echo "Running SDK contract test v2..." | ||
| @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ | ||
| -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/main/downloader/run.sh \ | ||
| | VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -skip-from testharness-suppressions.txt -debug $(TEST_HARNESS_PARAMS)" sh | ||
| -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \ | ||
| | VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For reviewers: The above block will be deleted and replaced with the below block after the below block is uncommented in a future PR. |
||
|
|
||
| # Uncomment this, update v3 version, and replace existing run-contract-tests once sdk-test-harness releases a version that includes FDv2 client contract tests. | ||
| # | ||
| # run-contract-tests: | ||
| # @echo "Running SDK contract test v2..." | ||
| # @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ | ||
| # -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \ | ||
| # | VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh | ||
| # @echo "Running SDK contract test v3..." | ||
| # @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ | ||
| # -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.0.0-alpha.4/downloader/run.sh \ | ||
| # | VERSION=v3.0.0-alpha.4 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh | ||
|
|
||
| contract-tests: build-contract-tests start-emulator start-contract-test-service run-contract-tests | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,7 @@ public static class SdkConfigParams { | |
| SdkConfigClientSideParams clientSide; | ||
| SdkConfigServiceEndpointParams serviceEndpoints; | ||
| SdkConfigHookParams hooks; | ||
| SdkConfigDataSystemParams dataSystem; | ||
| } | ||
|
|
||
| public static class SdkConfigStreamParams { | ||
|
|
@@ -102,6 +103,37 @@ public static class HookErrors { | |
| String afterTrack; | ||
| } | ||
|
|
||
| public static class SdkConfigDataSystemParams { | ||
| Boolean useDefaultDataSystem; | ||
| SdkConfigConnectionModeConfig connectionModeConfig; | ||
| /** | ||
| * FDv2 / data-system tests: pipelines when {@link #connectionModeConfig} does not define | ||
| * {@link SdkConfigConnectionModeConfig#customConnectionModes}. If both are present, custom | ||
| * connection modes take precedence and these lists are ignored. | ||
| */ | ||
| List<SdkConfigDataInitializer> initializers; | ||
| List<SdkConfigDataSynchronizer> synchronizers; | ||
| } | ||
|
|
||
| public static class SdkConfigConnectionModeConfig { | ||
| String initialConnectionMode; | ||
| Map<String, SdkConfigModeDefinition> customConnectionModes; | ||
| } | ||
|
|
||
| public static class SdkConfigModeDefinition { | ||
| List<SdkConfigDataInitializer> initializers; | ||
| List<SdkConfigDataSynchronizer> synchronizers; | ||
| } | ||
|
|
||
| public static class SdkConfigDataInitializer { | ||
| SdkConfigPollParams polling; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my current cache initializer work, I'll want to add a new SdkConfigCacheParams class and wire it up here? |
||
|
|
||
| public static class SdkConfigDataSynchronizer { | ||
| SdkConfigStreamParams streaming; | ||
| SdkConfigPollParams polling; | ||
| } | ||
|
|
||
| public static class CommandParams { | ||
| String command; | ||
| EvaluateFlagParams evaluate; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,16 +9,27 @@ | |
| import com.launchdarkly.sdk.LDValue; | ||
| import com.launchdarkly.sdk.android.Components; | ||
| import com.launchdarkly.sdk.android.ConfigHelper; | ||
| import com.launchdarkly.sdk.android.ConnectionMode; | ||
| import com.launchdarkly.sdk.android.DataSystemComponents; | ||
| import com.launchdarkly.sdk.android.LaunchDarklyException; | ||
| import com.launchdarkly.sdk.android.LDClient; | ||
| import com.launchdarkly.sdk.android.LDConfig; | ||
|
|
||
| import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.AutomaticModeSwitchingConfig; | ||
| import com.launchdarkly.sdk.android.integrations.ConnectionModeBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.DataSystemBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.Hook; | ||
| import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.PollingInitializerBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.PollingSynchronizerBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.StreamingSynchronizerBuilder; | ||
| import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; | ||
| import com.launchdarkly.sdk.android.subsystems.DataSourceBuilder; | ||
| import com.launchdarkly.sdk.android.subsystems.Initializer; | ||
| import com.launchdarkly.sdk.android.subsystems.Synchronizer; | ||
| import com.launchdarkly.sdk.json.JsonSerialization; | ||
|
|
||
| import com.launchdarkly.sdktest.Representations.CommandParams; | ||
|
|
@@ -37,6 +48,11 @@ | |
| import com.launchdarkly.sdktest.Representations.HookErrors; | ||
| import com.launchdarkly.sdktest.Representations.IdentifyEventParams; | ||
| import com.launchdarkly.sdktest.Representations.SdkConfigParams; | ||
| import com.launchdarkly.sdktest.Representations.SdkConfigDataSystemParams; | ||
| import com.launchdarkly.sdktest.Representations.SdkConfigConnectionModeConfig; | ||
| import com.launchdarkly.sdktest.Representations.SdkConfigModeDefinition; | ||
| import com.launchdarkly.sdktest.Representations.SdkConfigDataInitializer; | ||
| import com.launchdarkly.sdktest.Representations.SdkConfigDataSynchronizer; | ||
|
|
||
| import android.app.Application; | ||
|
|
||
|
|
@@ -268,26 +284,29 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, | |
| // to be affected by each other's cached flag values. | ||
| ConfigHelper.configureIsolatedInMemoryPersistence(builder); | ||
|
|
||
| if (params.polling != null && params.polling.baseUri != null) { | ||
| // Note that this property can be set even if streaming is enabled | ||
| endpoints.polling(params.polling.baseUri); | ||
| } | ||
|
|
||
| if (params.polling != null && params.streaming == null) { | ||
| PollingDataSourceBuilder pollingBuilder = Components.pollingDataSource(); | ||
| if (params.polling.pollIntervalMs != null) { | ||
| pollingBuilder.pollIntervalMillis(params.polling.pollIntervalMs.intValue()); | ||
| } | ||
| builder.dataSource(pollingBuilder); | ||
| } else if (params.streaming != null) { | ||
| if (params.streaming.baseUri != null) { | ||
| endpoints.streaming(params.streaming.baseUri); | ||
| if (params.dataSystem != null) { | ||
| configureDataSystem(builder, params.dataSystem); | ||
| } else { | ||
| if (params.polling != null && params.polling.baseUri != null) { | ||
| endpoints.polling(params.polling.baseUri); | ||
| } | ||
| StreamingDataSourceBuilder streamingBuilder = Components.streamingDataSource(); | ||
| if (params.streaming.initialRetryDelayMs != null) { | ||
| streamingBuilder.initialReconnectDelayMillis(params.streaming.initialRetryDelayMs.intValue()); | ||
|
|
||
| if (params.polling != null && params.streaming == null) { | ||
| PollingDataSourceBuilder pollingBuilder = Components.pollingDataSource(); | ||
| if (params.polling.pollIntervalMs != null) { | ||
| pollingBuilder.pollIntervalMillis(params.polling.pollIntervalMs.intValue()); | ||
| } | ||
| builder.dataSource(pollingBuilder); | ||
| } else if (params.streaming != null) { | ||
| if (params.streaming.baseUri != null) { | ||
| endpoints.streaming(params.streaming.baseUri); | ||
| } | ||
| StreamingDataSourceBuilder streamingBuilder = Components.streamingDataSource(); | ||
| if (params.streaming.initialRetryDelayMs != null) { | ||
| streamingBuilder.initialReconnectDelayMillis(params.streaming.initialRetryDelayMs.intValue()); | ||
| } | ||
| builder.dataSource(streamingBuilder); | ||
| } | ||
| builder.dataSource(streamingBuilder); | ||
| } | ||
|
|
||
| if (params.events == null) { | ||
|
|
@@ -373,6 +392,122 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, | |
| return builder.build(); | ||
| } | ||
|
|
||
| private void configureDataSystem(LDConfig.Builder builder, SdkConfigDataSystemParams dataSystem) { | ||
| if (Boolean.TRUE.equals(dataSystem.useDefaultDataSystem)) { | ||
| builder.dataSystem(Components.dataSystem()); | ||
| return; | ||
| } | ||
|
|
||
| SdkConfigConnectionModeConfig connModeConfig = dataSystem.connectionModeConfig; | ||
| boolean hasTopLevelPipelines = hasTopLevelDataSystemPipelines(dataSystem); | ||
|
|
||
| if (connModeConfig == null && !hasTopLevelPipelines) { | ||
| return; | ||
| } | ||
|
|
||
| DataSystemBuilder dsBuilder = Components.dataSystem(); | ||
|
|
||
| if (connModeConfig != null && connModeConfig.initialConnectionMode != null) { | ||
| dsBuilder.foregroundConnectionMode(connectionModeFromString(connModeConfig.initialConnectionMode)); | ||
| } | ||
|
|
||
| // at the time of writing this, we did not have contract tests that could test platform state changes, | ||
| // disabling automatic mode simplifies the behavior being tested | ||
| dsBuilder.automaticModeSwitching(AutomaticModeSwitchingConfig.disabled()); | ||
|
|
||
| // Prefer connectionModeConfig when the harness sends both that and top-level pipelines. | ||
| if (hasConnectionModeCustomPipelines(connModeConfig)) { | ||
| for (Map.Entry<String, SdkConfigModeDefinition> entry : connModeConfig.customConnectionModes.entrySet()) { | ||
| ConnectionMode mode = connectionModeFromString(entry.getKey()); | ||
| ConnectionModeBuilder modeBuilder = buildConnectionModeBuilder(entry.getValue()); | ||
| dsBuilder.customizeConnectionMode(mode, modeBuilder); | ||
| } | ||
| } else if (hasTopLevelPipelines) { | ||
| SdkConfigModeDefinition topLevel = new SdkConfigModeDefinition(); | ||
| topLevel.initializers = dataSystem.initializers; | ||
| topLevel.synchronizers = dataSystem.synchronizers; | ||
| dsBuilder.customizeConnectionMode(ConnectionMode.STREAMING, buildConnectionModeBuilder(topLevel)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Top-level pipelines hardcoded to STREAMING ignoring foreground modeMedium Severity When Additional Locations (1)Reviewed by Cursor Bugbot for commit 7b4efaf. Configure here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is right, looking at the code. Do you want to use connectionModeConfig.initialConnectionMode as the first parameter, but fall back to STREAMING if the initial connection mode is empty?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And should this issue be triggered by a contract test that exercises the |
||
| } | ||
|
|
||
| builder.dataSystem(dsBuilder); | ||
| } | ||
|
|
||
| private static boolean hasTopLevelDataSystemPipelines(SdkConfigDataSystemParams dataSystem) { | ||
| return (dataSystem.initializers != null && !dataSystem.initializers.isEmpty()) | ||
| || (dataSystem.synchronizers != null && !dataSystem.synchronizers.isEmpty()); | ||
| } | ||
|
|
||
| /** True when {@code connectionModeConfig.customConnectionModes} defines at least one mode pipeline. */ | ||
| private static boolean hasConnectionModeCustomPipelines(SdkConfigConnectionModeConfig connModeConfig) { | ||
| return connModeConfig != null | ||
| && connModeConfig.customConnectionModes != null | ||
| && !connModeConfig.customConnectionModes.isEmpty(); | ||
| } | ||
|
|
||
| private static ConnectionModeBuilder buildConnectionModeBuilder(SdkConfigModeDefinition modeDef) { | ||
| ConnectionModeBuilder modeBuilder = DataSystemComponents.customMode(); | ||
|
|
||
| if (modeDef.initializers != null) { | ||
| List<DataSourceBuilder<Initializer>> initList = new ArrayList<>(); | ||
| for (SdkConfigDataInitializer init : modeDef.initializers) { | ||
| if (init.polling != null) { | ||
| PollingInitializerBuilder initBuilder = DataSystemComponents.pollingInitializer(); | ||
| if (init.polling.baseUri != null) { | ||
| initBuilder.serviceEndpointsOverride( | ||
| Components.serviceEndpoints().polling(init.polling.baseUri)); | ||
| } | ||
| initList.add(initBuilder); | ||
| } | ||
| } | ||
| @SuppressWarnings("unchecked") | ||
| DataSourceBuilder<Initializer>[] initArray = initList.toArray(new DataSourceBuilder[0]); | ||
| modeBuilder.initializers(initArray); | ||
| } | ||
|
|
||
| if (modeDef.synchronizers != null) { | ||
| List<DataSourceBuilder<Synchronizer>> syncList = new ArrayList<>(); | ||
| for (SdkConfigDataSynchronizer sync : modeDef.synchronizers) { | ||
| if (sync.streaming != null) { | ||
| StreamingSynchronizerBuilder syncBuilder = DataSystemComponents.streamingSynchronizer(); | ||
| if (sync.streaming.initialRetryDelayMs != null) { | ||
| syncBuilder.initialReconnectDelayMillis(sync.streaming.initialRetryDelayMs.intValue()); | ||
| } | ||
| if (sync.streaming.baseUri != null) { | ||
| syncBuilder.serviceEndpointsOverride( | ||
| Components.serviceEndpoints().streaming(sync.streaming.baseUri)); | ||
| } | ||
| syncList.add(syncBuilder); | ||
| } else if (sync.polling != null) { | ||
| PollingSynchronizerBuilder syncBuilder = DataSystemComponents.pollingSynchronizer(); | ||
| if (sync.polling.pollIntervalMs != null) { | ||
| syncBuilder.pollIntervalMillis(sync.polling.pollIntervalMs.intValue()); | ||
| } | ||
| if (sync.polling.baseUri != null) { | ||
| syncBuilder.serviceEndpointsOverride( | ||
| Components.serviceEndpoints().polling(sync.polling.baseUri)); | ||
| } | ||
| syncList.add(syncBuilder); | ||
| } | ||
| } | ||
| @SuppressWarnings("unchecked") | ||
| DataSourceBuilder<Synchronizer>[] syncArray = syncList.toArray(new DataSourceBuilder[0]); | ||
| modeBuilder.synchronizers(syncArray); | ||
| } | ||
|
|
||
| return modeBuilder; | ||
| } | ||
|
|
||
| private static ConnectionMode connectionModeFromString(String name) { | ||
| switch (name) { | ||
| case "streaming": return ConnectionMode.STREAMING; | ||
| case "polling": return ConnectionMode.POLLING; | ||
| case "offline": return ConnectionMode.OFFLINE; | ||
| case "one-shot": return ConnectionMode.ONE_SHOT; | ||
| case "background": return ConnectionMode.BACKGROUND; | ||
| default: throw new IllegalArgumentException("Unknown connection mode: " + name); | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this function something that could be put into the ConnectionMode class? Each ConnectionMode has a defined |
||
|
|
||
| public void close() { | ||
| try { | ||
| client.close(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| streaming/fdv2/fallback to FDv1 handling |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"events/disabling" was skipped, but it is supported and has been for some time. I have updated it to not skip this.