Skip to content

fix: change sync-init flow to support project auto update on cache download #318

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

Merged
merged 7 commits into from
Mar 13, 2020
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
import com.optimizely.ab.config.DatafileProjectConfig;
import com.optimizely.ab.config.ProjectConfig;
import com.optimizely.ab.config.Variation;
import com.optimizely.ab.config.parser.ConfigParseException;
import com.optimizely.ab.event.EventHandler;
import com.optimizely.ab.event.EventProcessor;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;

import org.junit.Before;
import org.junit.Test;
Expand All @@ -60,9 +62,12 @@
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand All @@ -78,6 +83,7 @@ public class OptimizelyManagerTest {
private Logger logger;
private OptimizelyManager optimizelyManager;
private DefaultDatafileHandler defaultDatafileHandler;
private String defaultDatafile;

private String minDatafile = "{\n" +
"experiments: [ ],\n" +
Expand Down Expand Up @@ -106,8 +112,8 @@ public void setup() throws Exception {
.withEventHandler(eventHandler)
.withEventProcessor(eventProcessor)
.build(InstrumentationRegistry.getTargetContext());
String datafile = optimizelyManager.getDatafile(InstrumentationRegistry.getTargetContext(), R.raw.datafile);
ProjectConfig config = new DatafileProjectConfig.Builder().withDatafile(datafile).build();
defaultDatafile = optimizelyManager.getDatafile(InstrumentationRegistry.getTargetContext(), R.raw.datafile);
ProjectConfig config = new DatafileProjectConfig.Builder().withDatafile(defaultDatafile).build();

when(defaultDatafileHandler.getConfig()).thenReturn(config);
}
Expand Down Expand Up @@ -213,7 +219,8 @@ public void getDatafile() {
assertNotNull(datafile);
assertNotNull(optimizelyManager.getDatafileHandler());
}
@Test

@Test
public void initializeAsyncWithEnvironment() {
Logger logger = mock(Logger.class);
DatafileHandler datafileHandler = mock(DefaultDatafileHandler.class);
Expand Down Expand Up @@ -496,4 +503,225 @@ public void injectOptimizelyDoesNotDuplicateCallback() {
verify(logger).info("Sending Optimizely instance to listener");
verify(startListener).onStart(any(OptimizelyClient.class));
}

// Init Sync Flows

@Test
public void initializeSyncWithUpdateOnNewDatafileDisabled() {
boolean downloadToCache = true;
boolean updateConfigOnNewDatafiel = false;
int pollingInterval = 0; // disable polling

DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
Logger logger = mock(Logger.class);
Context context = InstrumentationRegistry.getTargetContext();

OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
null, null, null, null);

doAnswer(
new Answer<Object>() {
public Object answer(InvocationOnMock invocation) {
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
return null;
}
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));

OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);

try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//
}

assertEquals(client.getOptimizelyConfig().getRevision(), "7");
}

@Test
public void initializeSyncWithUpdateOnNewDatafileEnabled() {
boolean downloadToCache = true;
boolean updateConfigOnNewDatafiel = true;
int pollingInterval = 0; // disable polling

DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
Logger logger = mock(Logger.class);
Context context = InstrumentationRegistry.getTargetContext();

OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
null, null, null, null);

doAnswer(
new Answer<Object>() {
public Object answer(InvocationOnMock invocation) {
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
return null;
}
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));

OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);

try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//
}

assertEquals(client.getOptimizelyConfig().getRevision(), "241");
}

@Test
public void initializeSyncWithDownloadToCacheDisabled() {
boolean downloadToCache = false;
boolean updateConfigOnNewDatafiel = true;
int pollingInterval = 0; // disable polling

DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
Logger logger = mock(Logger.class);
Context context = InstrumentationRegistry.getTargetContext();

OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
null, null, null, null);

doAnswer(
new Answer<Object>() {
public Object answer(InvocationOnMock invocation) {
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
return null;
}
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));

OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);

try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//
}

assertEquals(client.getOptimizelyConfig().getRevision(), "7");
}

@Test
public void initializeSyncWithUpdateOnNewDatafileDisabledWithPeriodicPollingEnabled() {
boolean downloadToCache = true;
boolean updateConfigOnNewDatafiel = false;
int pollingInterval = 30; // enable polling

DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
Logger logger = mock(Logger.class);
Context context = InstrumentationRegistry.getTargetContext();

OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
null, null, null, null);

doAnswer(
new Answer<Object>() {
public Object answer(InvocationOnMock invocation) {
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
return null;
}
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));

OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);

try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//
}

// when periodic polling enabled, project config always updated on cache datafile update (regardless of "updateConfigOnNewDatafile" setting)
assertEquals(client.getOptimizelyConfig().getRevision(), "241");
}

@Test
public void initializeSyncWithUpdateOnNewDatafileEnabledWithPeriodicPollingEnabled() {
boolean downloadToCache = true;
boolean updateConfigOnNewDatafiel = true;
int pollingInterval = 30; // enable polling

DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
Logger logger = mock(Logger.class);
Context context = InstrumentationRegistry.getTargetContext();

OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
null, null, null, null);

doAnswer(
new Answer<Object>() {
public Object answer(InvocationOnMock invocation) {
String newDatafile = manager.getDatafile(context, R.raw.datafile_api);
datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile);
return null;
}
}).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class));

OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafiel);

try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//
}

// when periodic polling enabled, project config always updated on cache datafile update (regardless of "updateConfigOnNewDatafile" setting)
assertEquals(client.getOptimizelyConfig().getRevision(), "241");
}

@Test
public void initializeSyncWithResourceDatafileNoCache() {
boolean downloadToCache = true;
boolean updateConfigOnNewDatafiel = true;
int pollingInterval = 30; // enable polling

DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
Logger logger = mock(Logger.class);
Context context = InstrumentationRegistry.getTargetContext();

OptimizelyManager manager = spy(new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
null, null, null, null));

datafileHandler.removeSavedDatafile(context, manager.getDatafileConfig());
OptimizelyClient client = manager.initialize(context, R.raw.datafile, downloadToCache, updateConfigOnNewDatafiel);

verify(manager).initialize(eq(context), eq(defaultDatafile), eq(downloadToCache), eq(updateConfigOnNewDatafiel));
}

@Test
public void initializeSyncWithResourceDatafileNoCacheWithDefaultParams() {
boolean downloadToCache = true;
boolean updateConfigOnNewDatafiel = true;
int pollingInterval = 30; // enable polling

DefaultDatafileHandler datafileHandler = spy(new DefaultDatafileHandler());
Logger logger = mock(Logger.class);
Context context = InstrumentationRegistry.getTargetContext();

OptimizelyManager manager = spy(new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
null, null, null, null));

datafileHandler.removeSavedDatafile(context, manager.getDatafileConfig());
OptimizelyClient client = manager.initialize(context, R.raw.datafile);

verify(manager).initialize(eq(context), eq(defaultDatafile), eq(true), eq(false));
}


// Utils

void mockProjectConfig(DefaultDatafileHandler datafileHandler, String datafile) {
ProjectConfig config = null;
try {
config = new DatafileProjectConfig.Builder().withDatafile(datafile).build();
when(datafileHandler.getConfig()).thenReturn(config);
} catch (ConfigParseException e) {
e.printStackTrace();
}
}

}
1 change: 1 addition & 0 deletions android-sdk/src/debug/res/raw/datafile_api
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"4","rollouts":[],"anonymizeIP":true,"botFiltering":true,"projectId":"10431130345","variables":[],"featureFlags":[{"experimentIds":["10390977673"],"id":"4482920077","key":"feature_1","rolloutId":"","variables":[{"defaultValue":"42","id":"2687470095","key":"i_42","type":"integer"},{"defaultValue":"4.2","id":"2689280165","key":"d_4_2","type":"double"},{"defaultValue":"true","id":"2689660112","key":"b_true","type":"boolean"},{"defaultValue":"foo","id":"2696150066","key":"s_foo","type":"string"}]},{"experimentIds":["10420810910"],"id":"4482920078","key":"feature_2","rolloutId":"","variables":[]}],"experiments":[{"status":"Running","key":"exp_with_audience","layerId":"10420273888","trafficAllocation":[{"entityId":"10389729780","endOfRange":10000}],"audienceIds":[],"variations":[{"variables":[],"featureEnabled":true,"id":"10389729780","key":"a"},{"variables":[],"id":"10416523121","key":"b"}],"forcedVariations":{},"id":"10390977673"},{"status":"Running","key":"exp_no_audience","layerId":"10417730432","trafficAllocation":[{"entityId":"10418551353","endOfRange":10000}],"audienceIds":[],"variations":[{"variables":[],"id":"10418551353","key":"variation_with_traffic"},{"variables":[],"id":"10418510624","key":"variation_no_traffic"}],"forcedVariations":{},"id":"10420810910"}],"audiences":[{"id":"10413101795","conditions":"[\"and\", [\"or\", [\"or\", {\"type\": \"custom_attribute\", \"name\": \"testvar\", \"value\": \"testvalue\"}]]]","name":"testvalue_audience"}],"groups":[],"attributes":[{"id":"10401066170","key":"testvar"}],"accountId":"10367498574","events":[{"experimentIds":["10420810910"],"id":"10404198134","key":"event1"},{"experimentIds":["10420810910","10390977673"],"id":"10404198135","key":"event_multiple_running_exp_attached"}],"revision":"241"}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,24 @@ public OptimizelyClient initialize(@NonNull Context context, @NonNull String dat
* @return an {@link OptimizelyClient} instance
*/
public OptimizelyClient initialize(@NonNull Context context, @Nullable String datafile, boolean downloadToCache) {
return initialize(context, datafile, downloadToCache, false);
}

/**
* Initialize Optimizely Synchronously using the datafile passed in.
* It should be noted that even though it initiates a download of the datafile to cache, this method does not use that cached datafile.
* You can always test if a datafile exists in cache with {@link #isDatafileCached(Context)}.
* <p>
* Instantiates and returns an {@link OptimizelyClient} instance. It will also cache the instance
* for future lookups via getClient
*
* @param context any {@link Context} instance
* @param datafile the datafile used to initialize the OptimizelyClient.
* @param downloadToCache to check if datafile should get updated in cache after initialization.
* @param updateConfigOnNewDatafile When a new datafile is fetched from the server in the background thread, the SDK will be updated with the new datafile immediately if this value is set to true. When it's set to false (default), the new datafile is cached and will be used when the SDK is started again.
* @return an {@link OptimizelyClient} instance
*/
public OptimizelyClient initialize(@NonNull Context context, @Nullable String datafile, boolean downloadToCache, boolean updateConfigOnNewDatafile) {
if (!isAndroidVersionSupported()) {
return optimizelyClient;
}
Expand All @@ -208,8 +226,9 @@ public OptimizelyClient initialize(@NonNull Context context, @Nullable String da
} catch (Error e) {
logger.error("Unable to build OptimizelyClient instance", e);
}
if(downloadToCache){
datafileHandler.downloadDatafile(context, datafileConfig, null);

if (downloadToCache) {
datafileHandler.downloadDatafileToCache(context, datafileConfig, updateConfigOnNewDatafile);
}

return optimizelyClient;
Expand All @@ -226,17 +245,19 @@ public OptimizelyClient initialize(@NonNull Context context, @Nullable String da
*
* @param context any {@link Context} instance
* @param datafileRes the R id that the data file is located under.
* @param downloadToCache to check if datafile should get updated in cache after initialization.
* @param updateConfigOnNewDatafile When a new datafile is fetched from the server in the background thread, the SDK will be updated with the new datafile immediately if this value is set to true. When it's set to false (default), the new datafile is cached and will be used when the SDK is started again.
* @return an {@link OptimizelyClient} instance
*/
@NonNull
public OptimizelyClient initialize(@NonNull Context context, @RawRes Integer datafileRes) {
public OptimizelyClient initialize(@NonNull Context context, @RawRes Integer datafileRes, boolean downloadToCache, boolean updateConfigOnNewDatafile) {
try {

String datafile;
Boolean datafileInCache = isDatafileCached(context);
datafile = getDatafile(context, datafileRes);

optimizelyClient = initialize(context, datafile, true);
optimizelyClient = initialize(context, datafile, downloadToCache, updateConfigOnNewDatafile);
if (datafileInCache) {
cleanupUserProfileCache(getUserProfileService());
}
Expand All @@ -248,6 +269,24 @@ public OptimizelyClient initialize(@NonNull Context context, @RawRes Integer dat
return optimizelyClient;
}

/**
* Initialize Optimizely Synchronously by loading the resource, use it to initialize Optimizely,
* and downloading the latest datafile from the CDN in the background to cache.
* <p>
* Instantiates and returns an {@link OptimizelyClient} instance using the datafile cached on disk
* if not available then it will expect that raw data file should exist on given id.
* and initialize using raw file. Will also cache the instance
* for future lookups via getClient. The datafile should be stored in res/raw.
*
* @param context any {@link Context} instance
* @param datafileRes the R id that the data file is located under.
* @return an {@link OptimizelyClient} instance
*/
@NonNull
public OptimizelyClient initialize(@NonNull Context context, @RawRes Integer datafileRes) {
return initialize(context, datafileRes, true, false);
}

private void cleanupUserProfileCache(UserProfileService userProfileService) {
final DefaultUserProfileService defaultUserProfileService;
if (userProfileService instanceof DefaultUserProfileService) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public interface DatafileHandler {
*/
void downloadDatafile(Context context, DatafileConfig datafileConfig, DatafileLoadedListener listener);

default void downloadDatafileToCache(final Context context, DatafileConfig datafileConfig, boolean updateConfigOnNewDatafile) {
downloadDatafile(context, datafileConfig, null);
}

/**
* Start background updates to the project datafile .
*
Expand Down
Loading