Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android-sdk-framework/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ android {
}

dependencies {
api 'cloud.eppo:eppo-sdk-framework:0.1.0-SNAPSHOT'
api 'cloud.eppo:eppo-sdk-framework:0.1.0'

api 'com.google.code.gson:gson:2.10.1'
api 'org.slf4j:slf4j-android:1.7.36'
compileOnly 'org.jetbrains:annotations:24.0.0'

testImplementation 'cloud.eppo:sdk-common-jvm:4.0.0-SNAPSHOT'
testImplementation 'cloud.eppo:sdk-common-jvm:4.0.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.14.2'
testImplementation 'org.robolectric:robolectric:4.12.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package cloud.eppo.android.framework;

import static cloud.eppo.android.framework.util.Utils.logTag;
import static cloud.eppo.android.framework.util.Utils.safeCacheKey;
import static org.junit.Assert.assertNotNull;

import android.app.Application;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import cloud.eppo.android.framework.storage.CachingConfigurationStore;
import cloud.eppo.android.framework.storage.ConfigurationCodec;
import cloud.eppo.android.framework.storage.FileBackedConfigStore;
import cloud.eppo.api.Configuration;
import cloud.eppo.http.EppoConfigurationClient;
import cloud.eppo.parser.ConfigurationParser;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
Expand All @@ -27,30 +33,86 @@ public class EppoClientPollingTest {
private static final String TAG = logTag(EppoClientPollingTest.class);
private static final String DUMMY_API_KEY = "mock-api-key";

@Mock private ConfigurationParser<JsonNode> mockConfigParser;
@Mock private ConfigurationParser<Configuration, JsonNode> mockConfigParser;

@Mock private EppoConfigurationClient mockConfigClient;

private CachingConfigurationStore<Configuration> configurationStore =
new FileBackedConfigStore<>(
ApplicationProvider.getApplicationContext(),
safeCacheKey(DUMMY_API_KEY),
new ConfigurationCodec.Default());

@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
}

private static class TestBuilder
extends AndroidBaseClient.Builder<
TestBuilder, AndroidBaseClient<Configuration, JsonNode>, Configuration, JsonNode> {
protected TestBuilder(
@NotNull String apiKey,
@NotNull Application application,
@NotNull ConfigurationParser<Configuration, JsonNode> configurationParser,
@NotNull CachingConfigurationStore<Configuration> configStore,
@NotNull EppoConfigurationClient configurationClient) {
super(
TestBuilder.class,
apiKey,
application,
configurationParser,
configStore,
configurationClient);
}

@Override
protected AndroidBaseClient<Configuration, JsonNode> newInstance(
String apiKey,
String sdkName,
String sdkVersion,
@org.jetbrains.annotations.Nullable String apiBaseUrl,
@org.jetbrains.annotations.Nullable cloud.eppo.logging.AssignmentLogger assignmentLogger,
CachingConfigurationStore<Configuration> configurationStore,
boolean isGracefulMode,
boolean expectObfuscatedConfig,
@org.jetbrains.annotations.Nullable java.util.concurrent.CompletableFuture<Configuration> initialConfiguration,
@org.jetbrains.annotations.Nullable cloud.eppo.api.IAssignmentCache assignmentCache,
cloud.eppo.parser.ConfigurationParser<Configuration, JsonNode> configurationParser,
cloud.eppo.http.EppoConfigurationClient configurationClient) {
return new AndroidBaseClient<Configuration, JsonNode>(
apiKey,
sdkName,
sdkVersion,
apiBaseUrl,
assignmentLogger,
configurationStore,
isGracefulMode,
expectObfuscatedConfig,
initialConfiguration,
assignmentCache,
configurationParser,
configurationClient) {};
}
}

/**
* Builds a client in offline mode with polling enabled.
*
* @param pollingIntervalMs Polling interval in milliseconds
* @return Initialized EppoClient
*/
private AndroidBaseClient<JsonNode> buildOfflineClientWithPolling(long pollingIntervalMs)
throws ExecutionException, InterruptedException {
private AndroidBaseClient<Configuration, JsonNode> buildOfflineClientWithPolling(
long pollingIntervalMs) throws ExecutionException, InterruptedException {
// Use an empty configuration for offline mode
CompletableFuture<Configuration> initialConfig =
CompletableFuture.completedFuture(Configuration.emptyConfig());

return new AndroidBaseClient.Builder<>(
return new TestBuilder(
DUMMY_API_KEY,
ApplicationProvider.getApplicationContext(),
mockConfigParser,
configurationStore,
mockConfigClient)
.forceReinitialize(true)
.offlineMode(true)
Expand All @@ -67,15 +129,16 @@ private AndroidBaseClient<JsonNode> buildOfflineClientWithPolling(long pollingIn
*
* @return Initialized EppoClient
*/
private AndroidBaseClient<JsonNode> buildOfflineClientWithoutPolling()
private AndroidBaseClient<Configuration, JsonNode> buildOfflineClientWithoutPolling()
throws ExecutionException, InterruptedException {
CompletableFuture<Configuration> initialConfig =
CompletableFuture.completedFuture(Configuration.emptyConfig());

return new AndroidBaseClient.Builder<>(
return new TestBuilder(
DUMMY_API_KEY,
ApplicationProvider.getApplicationContext(),
mockConfigParser,
configurationStore,
mockConfigClient)
.forceReinitialize(true)
.offlineMode(true)
Expand All @@ -88,7 +151,8 @@ private AndroidBaseClient<JsonNode> buildOfflineClientWithoutPolling()

@Test
public void testPauseAndResumePolling() throws ExecutionException, InterruptedException {
AndroidBaseClient<JsonNode> androidBaseClient = buildOfflineClientWithPolling(100);
AndroidBaseClient<Configuration, JsonNode> androidBaseClient =
buildOfflineClientWithPolling(100);
assertNotNull("Client should be initialized", androidBaseClient);

// Test pause
Expand All @@ -111,7 +175,8 @@ public void testPauseAndResumePolling() throws ExecutionException, InterruptedEx

@Test
public void testResumePollingWithoutStarting() throws ExecutionException, InterruptedException {
AndroidBaseClient<JsonNode> androidBaseClient = buildOfflineClientWithoutPolling();
AndroidBaseClient<Configuration, JsonNode> androidBaseClient =
buildOfflineClientWithoutPolling();
assertNotNull("Client should be initialized", androidBaseClient);

// Try to resume polling (should log warning and not crash per EppoClient.java:436-441)
Expand All @@ -126,7 +191,8 @@ public void testResumePollingWithoutStarting() throws ExecutionException, Interr

@Test
public void testMultiplePauseResumeCycles() throws ExecutionException, InterruptedException {
AndroidBaseClient<JsonNode> androidBaseClient = buildOfflineClientWithPolling(100);
AndroidBaseClient<Configuration, JsonNode> androidBaseClient =
buildOfflineClientWithPolling(100);
assertNotNull("Client should be initialized", androidBaseClient);

// First cycle
Expand Down Expand Up @@ -160,7 +226,8 @@ public void testMultiplePauseResumeCycles() throws ExecutionException, Interrupt
@Test
public void testPauseResumeSequenceDoesNotCrash()
throws ExecutionException, InterruptedException {
AndroidBaseClient<JsonNode> androidBaseClient = buildOfflineClientWithPolling(50);
AndroidBaseClient<Configuration, JsonNode> androidBaseClient =
buildOfflineClientWithPolling(50);

// Various sequences that should all work without crashing
androidBaseClient.pausePolling();
Expand All @@ -182,7 +249,8 @@ public void testPauseResumeSequenceDoesNotCrash()

@Test
public void testPollingNotEnabledAndResume() throws ExecutionException, InterruptedException {
AndroidBaseClient<JsonNode> androidBaseClient = buildOfflineClientWithoutPolling();
AndroidBaseClient<Configuration, JsonNode> androidBaseClient =
buildOfflineClientWithoutPolling();

// Pause should be safe even if not polling
androidBaseClient.pausePolling();
Expand All @@ -200,7 +268,8 @@ public void testPollingNotEnabledAndResume() throws ExecutionException, Interrup

@Test
public void testPauseAfterInitDoesNotCrash() throws ExecutionException, InterruptedException {
AndroidBaseClient<JsonNode> androidBaseClient = buildOfflineClientWithPolling(100);
AndroidBaseClient<Configuration, JsonNode> androidBaseClient =
buildOfflineClientWithPolling(100);

// Immediately pause after initialization
androidBaseClient.pausePolling();
Expand Down
Loading
Loading