Skip to content

Commit

Permalink
Merge 6e12a8c into 0fe8385
Browse files Browse the repository at this point in the history
  • Loading branch information
qdpham13 authored Jan 3, 2024
2 parents 0fe8385 + 6e12a8c commit 325b245
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 3 deletions.
106 changes: 104 additions & 2 deletions FirebaseRemoteConfig/Sources/RCNConfigContent.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

#import "FirebaseCore/Extension/FirebaseCoreInternal.h"

static NSString *const kAffectedParameterKeys = @"affectedParameterKeys";

@implementation RCNConfigContent {
/// Active config data that is currently used.
NSMutableDictionary *_activeConfig;
Expand Down Expand Up @@ -398,13 +400,108 @@ - (BOOL)checkAndWaitForInitialDatabaseLoad {
return true;
}

/// Load active and fetched experiment payloads and return them in a map.
- (NSDictionary<NSString *, NSMutableArray<NSData *> *> *)loadExperimentsPayloads {
__block NSMutableArray *activeExperimentPayloads = [[NSMutableArray alloc] init];
__block NSMutableArray *experimentPayloads = [[NSMutableArray alloc] init];

/// Load experiments from DB.
RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
if (result[@RCNExperimentTableKeyPayload]) {
experimentPayloads = [result[@RCNExperimentTableKeyPayload] mutableCopy];
}
if (result[@RCNExperimentTableKeyActivePayload]) {
activeExperimentPayloads = [result[@RCNExperimentTableKeyActivePayload] mutableCopy];
}
};
[_DBManager loadExperimentWithCompletionHandler:completionHandler];

return @{
@RCNExperimentTableKeyPayload : experimentPayloads,
@RCNExperimentTableKeyActivePayload : activeExperimentPayloads
};
}

/// Creates a map where the key is the config key and the value if the experiment description.
- (NSMutableDictionary *)createExperimentsMap:(NSMutableArray<NSData *> *)experiments {
NSMutableDictionary<NSString *, NSMutableDictionary *> *experimentsMap =
[[NSMutableDictionary alloc] init];

/// Iterate through all the experiments and check if they contain `affectedParameterKeys`.
for (NSData *experiment in experiments) {
NSError *error;
NSDictionary *experimentJSON =
[NSJSONSerialization JSONObjectWithData:experiment
options:NSJSONReadingMutableContainers
error:&error];
if (!error && experimentJSON) {
if ([experimentJSON objectForKey:kAffectedParameterKeys]) {
NSMutableArray *configKeys =
(NSMutableArray *)[experimentJSON objectForKey:kAffectedParameterKeys];
NSMutableDictionary *experimentCopy = [experimentJSON mutableCopy];
/// Remove `affectedParameterKeys` because the values come out of order and could affect the
/// diffing.
[experimentCopy removeObjectForKey:kAffectedParameterKeys];

/// Map experiments to config keys.
for (NSString *key in configKeys) {
[experimentsMap setObject:experimentCopy forKey:key];
}
}
}
}

return experimentsMap;
}

/// Returns keys that were affected by experiment changes.
- (NSMutableSet<NSString *> *)
getKeysAffectedByChangedExperiments:(NSMutableArray *)activeExperimentPayloads
fetchedExperimentPayloads:(NSMutableArray *)experimentPayloads {
NSMutableSet<NSString *> *changedKeys = [[NSMutableSet alloc] init];

/// Create config keys to experiments map.
NSMutableDictionary *activeExperimentsMap = [self createExperimentsMap:activeExperimentPayloads];
NSMutableDictionary *fetchedExperimentsMap = [self createExperimentsMap:experimentPayloads];

/// Iterate through active experiment's keys and compare them to fetched experiment's keys.
for (NSString *key in [activeExperimentsMap allKeys]) {
if (![fetchedExperimentsMap objectForKey:key]) {
[changedKeys addObject:key];
} else {
if (![[activeExperimentsMap objectForKey:key]
isEqualToDictionary:[fetchedExperimentsMap objectForKey:key]]) {
[changedKeys addObject:key];
}
}
}

/// Iterate through fetched experiment's keys and compare them to active experiment's keys.
for (NSString *key in [fetchedExperimentsMap allKeys]) {
if (![activeExperimentsMap objectForKey:key]) {
[changedKeys addObject:key];
} else {
if (![[fetchedExperimentsMap objectForKey:key]
isEqualToDictionary:[activeExperimentsMap objectForKey:key]]) {
[changedKeys addObject:key];
}
}
}

return changedKeys;
}

// Compare fetched config with active config and output what has changed
- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace {
// TODO: handle diff in experiment metadata

FIRRemoteConfigUpdate *configUpdate;
NSMutableSet<NSString *> *updatedKeys = [[NSMutableSet alloc] init];

NSDictionary *experiments = [self loadExperimentsPayloads];
NSMutableSet *changedExperimentKeys = [self
getKeysAffectedByChangedExperiments:[experiments
objectForKey:@RCNExperimentTableKeyActivePayload]
fetchedExperimentPayloads:[experiments objectForKey:@RCNExperimentTableKeyPayload]];

NSDictionary *fetchedConfig =
_fetchedConfig[FIRNamespace] ? _fetchedConfig[FIRNamespace] : [[NSDictionary alloc] init];
NSDictionary *activeConfig =
Expand Down Expand Up @@ -439,6 +536,11 @@ - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace
}
}

// Add params affected by changed experiments.
for (NSString *key in changedExperimentKeys) {
[updatedKeys addObject:key];
}

configUpdate = [[FIRRemoteConfigUpdate alloc] initWithUpdatedKeys:updatedKeys];
return configUpdate;
}
Expand Down
151 changes: 151 additions & 0 deletions FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"

@interface RCNConfigContent (Testing)
- (NSMutableSet<NSString *> *)
getKeysAffectedByChangedExperiments:(NSMutableArray *)activeExperimentPayloads
fetchedExperimentPayloads:(NSMutableArray *)experimentPayloads;
- (BOOL)checkAndWaitForInitialDatabaseLoad;
@end

Expand Down Expand Up @@ -349,6 +352,129 @@ - (void)testConfigUpdate_noChange_emptyResponse {
XCTAssertTrue([update updatedKeys].count == 0);
}

- (void)testConfigUpdate_noParamChange_butExperimentChange {
NSString *namespace = @"test_namespace";
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *experimentKeys = [[NSMutableSet alloc] init];
[experimentKeys addObject:@"key_2"];
id configMock = OCMPartialMock(configContent);
OCMStub([configMock getKeysAffectedByChangedExperiments:OCMOCK_ANY
fetchedExperimentPayloads:OCMOCK_ANY])
.andReturn(experimentKeys);

// populate fetched config
NSMutableDictionary *fetchResponse =
[self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil];
[configMock updateConfigContentWithResponse:fetchResponse forNamespace:namespace];

// active config is the same as fetched config
FIRRemoteConfigValue *value =
[[FIRRemoteConfigValue alloc] initWithData:[@"value1" dataUsingEncoding:NSUTF8StringEncoding]
source:FIRRemoteConfigSourceRemote];
NSDictionary *namespaceToConfig = @{namespace : @{@"key1" : value}};
[configMock copyFromDictionary:namespaceToConfig
toSource:RCNDBSourceActive
forNamespace:namespace];

FIRRemoteConfigUpdate *update = [configMock getConfigUpdateForNamespace:namespace];

XCTAssertTrue([update updatedKeys].count == 1);
XCTAssertTrue([[update updatedKeys] containsObject:@"key_2"]);
}

- (void)testExperimentDiff_addedExperiment {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];

NSError *dataError;
NSMutableDictionary *payload =
[NSJSONSerialization JSONObjectWithData:payloadData1
options:NSJSONReadingMutableContainers
error:&dataError];
[payload setValue:@"exp_2" forKey:@"experimentId"];
NSError *jsonError;
NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
options:kNilOptions
error:&jsonError];
NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_changedExperimentMetadata {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];

NSError *dataError;
NSMutableDictionary *payload =
[NSJSONSerialization JSONObjectWithData:payloadData1
options:NSJSONReadingMutableContainers
error:&dataError];
[payload setValue:@"var_2" forKey:@"variantId"];
NSError *jsonError;
NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
options:kNilOptions
error:&jsonError];
NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_changedExperimentKeys {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];

NSError *dataError;
NSMutableDictionary *payload =
[NSJSONSerialization JSONObjectWithData:payloadData1
options:NSJSONReadingMutableContainers
error:&dataError];
[payload setValue:@[ @"test_key_1", @"test_key_2" ] forKey:@"affectedParameterKeys"];
NSError *jsonError;
NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
options:kNilOptions
error:&jsonError];
NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys containsObject:@"test_key_2"]);
}

- (void)testExperimentDiff_deletedExperiment {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
NSMutableArray *experimentPayloads = [@[] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_noChange {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
NSMutableArray *experimentPayloads = [@[ payloadData1 ] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys count] == 0);
}

- (void)testConfigUpdate_paramAdded_returnsNewKey {
NSString *namespace = @"test_namespace";
NSString *newParam = @"key2";
Expand Down Expand Up @@ -501,4 +627,29 @@ - (NSMutableDictionary *)createFetchResponseWithConfigEntries:(NSDictionary *)co
return fetchResponse;
}

+ (NSData *)payloadDataFromTestFile {
#if SWIFT_PACKAGE
NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
#else
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
#endif
NSString *testJsonDataFilePath = [bundle pathForResource:@"TestABTPayload" ofType:@"txt"];
NSError *readTextError = nil;
NSString *fileText = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath
encoding:NSUTF8StringEncoding
error:&readTextError];

NSData *fileData = [fileText dataUsingEncoding:kCFStringEncodingUTF8];

NSError *jsonDictionaryError = nil;
NSMutableDictionary *jsonDictionary =
[[NSJSONSerialization JSONObjectWithData:fileData
options:kNilOptions
error:&jsonDictionaryError] mutableCopy];
NSError *jsonDataError = nil;
return [NSJSONSerialization dataWithJSONObject:jsonDictionary
options:kNilOptions
error:&jsonDataError];
}

@end
3 changes: 2 additions & 1 deletion FirebaseRemoteConfig/Tests/Unit/TestABTPayload.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
{
"experimentId": "exp_1"
}
]
],
"affectedParameterKeys": ["test_key_1"]
}

0 comments on commit 325b245

Please sign in to comment.