From f853b61828db7c968c5513e83648f8273f58d8e7 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Tue, 9 May 2023 13:56:09 -0700 Subject: [PATCH 01/28] Add activated experiment metadata and payload keys to experiments table --- .../Sources/RCNConfigDefines.h | 2 + .../Sources/RCNConfigExperiment.m | 52 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h index 9181355bc9d..c13fa5b51e8 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h @@ -30,5 +30,7 @@ #define RCNExperimentTableKeyPayload "experiment_payload" #define RCNExperimentTableKeyMetadata "experiment_metadata" +#define RCNExperimentTableKeyActiveMetadata "experiment_active_metadata" +#define RCNExperimentTableKeyActivePayload "experiment_active_payload" #endif diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 9a2260ba622..ca63e4dcdaf 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -32,7 +32,11 @@ @interface RCNConfigExperiment () NSMutableArray *experimentPayloads; ///< Experiment payloads. @property(nonatomic, strong) NSMutableDictionary *experimentMetadata; ///< Experiment metadata -@property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. +@property(nonatomic, strong) + NSMutableArray *activatedExperimentPayloads; ///< Activated experiment payloads. +@property(nonatomic, strong) NSMutableDictionary + *activatedExperimentMetadata; ///< Activated experiment metadata +@property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. @property(nonatomic, strong) FIRExperimentController *experimentController; @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter; @end @@ -89,6 +93,26 @@ - (void)loadExperimentFromTable { if (result[@RCNExperimentTableKeyMetadata]) { strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy]; } + + if (result[@RCNExperimentTableKeyActivePayload]) { + [strongSelf->_activatedExperimentPayloads removeAllObjects]; + for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) { + NSError *error; + id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment + options:kNilOptions + error:&error]; + if (!experimentPayloadJSON || error) { + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031", + @"Activated experiment payload could not be parsed as JSON."); + } else { + [strongSelf->_activatedExperimentPayloads addObject:experiment]; + } + } + } + if (result[@RCNExperimentTableKeyActiveMetadata]) { + strongSelf->_activatedExperimentMetadata = + [result[@RCNExperimentTableKeyActiveMetadata] mutableCopy]; + } }; [_DBManager loadExperimentWithCompletionHandler:completionHandler]; } @@ -157,6 +181,32 @@ - (void)updateExperimentStartTime { completionHandler:nil]; } +- (void)updateActiveExperiments { + /// Put current fetched experiment metadata into activated experiment DB. + NSError *error; + NSData *serializedExperimentMetadata = + [NSJSONSerialization dataWithJSONObject:_experimentMetadata + options:NSJSONWritingPrettyPrinted + error:&error]; + + if (error) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000092", + @"Invalid activated experiment metadata to be serialized."); + return; + } else { + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActiveMetadata + value:serializedExperimentMetadata + completionHandler:nil]; + } + + /// Put current fetched experiment payloads into activated experiment DB. + for (NSData *data in _experimentPayloads) { + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload + value:data + completionHandler:nil]; + } +} + - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime { return [self.experimentController latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime From 9f1cea063a263aa76fc7c38a9184d18a74c897b7 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Tue, 9 May 2023 14:15:55 -0700 Subject: [PATCH 02/28] Call updateActivatedExperiments in activation method --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index ca63e4dcdaf..fabe4744a51 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -155,6 +155,9 @@ - (void)updateExperimentsWithHandler:(void (^)(NSError *_Nullable))handler { lastStartTime:lastStartTime payloads:_experimentPayloads completionHandler:handler]; + + /// Update activated experiments payload and metadata in DB. + [self updateActiveExperiments]; } - (void)updateExperimentStartTime { From ceae3556d7113a0bc1f2d8a50a36407368208abe Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Thu, 11 May 2023 09:22:03 -0700 Subject: [PATCH 03/28] Update key name for experiments table --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index fabe4744a51..d0ef6c39c87 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -204,7 +204,7 @@ - (void)updateActiveExperiments { /// Put current fetched experiment payloads into activated experiment DB. for (NSData *data in _experimentPayloads) { - [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload value:data completionHandler:nil]; } From e7b92b177121ea0f0b78c341555c872547d05f1b Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Thu, 11 May 2023 13:31:54 -0700 Subject: [PATCH 04/28] Update test to include activation call --- FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m index f69089dfa80..78bd7c06ca8 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -41,6 +41,7 @@ @interface RCNConfigExperiment () @property(nonatomic, strong) RCNConfigDBManager *DBManager; - (NSTimeInterval)updateExperimentStartTime; - (void)loadExperimentFromTable; +- (void)updateActiveExperiments; @end @interface RCNConfigExperimentTest : XCTestCase { @@ -232,6 +233,7 @@ - (void)testUpdateExperiments { XCTAssertNil(error); XCTAssertEqualObjects(experiment.experimentMetadata[@"last_experiment_start_time"], @(12345678)); + OCMVerify([experiment updateActiveExperiments]); }]; } From db6aff19bb1fb2843661dce7aca1d921d49463ff Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Thu, 11 May 2023 18:33:19 -0700 Subject: [PATCH 05/28] Update DB manager for Active experiments payload and metadata --- .../Sources/RCNConfigDBManager.m | 58 ++++++++- .../Sources/RCNConfigExperiment.m | 9 +- .../Tests/Unit/RCNConfigDBManagerTest.m | 110 ++++++++++++++++++ 3 files changed, 174 insertions(+), 3 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index c245f2a5ca5..8539e70e3dd 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -523,6 +523,9 @@ - (BOOL)insertExperimentTableWithKey:(NSString *)key value:(NSData *)dataValue { if ([key isEqualToString:@RCNExperimentTableKeyMetadata]) { return [self updateExperimentMetadata:dataValue]; } + if ([key isEqualToString:@RCNExperimentTableKeyActiveMetadata]) { + return [self updateActivatedExperimentMetadata:dataValue]; + } RCN_MUST_NOT_BE_MAIN_THREAD(); const char *SQL = "INSERT INTO " RCNTableNameExperiment " (key, value) values (?, ?)"; @@ -576,6 +579,35 @@ - (BOOL)updateExperimentMetadata:(NSData *)dataValue { return YES; } +- (BOOL)updateActivatedExperimentMetadata:(NSData *)dataValue { + RCN_MUST_NOT_BE_MAIN_THREAD(); + const char *SQL = "INSERT OR REPLACE INTO " RCNTableNameExperiment + " (_id, key, value) values ((SELECT _id from " RCNTableNameExperiment + " WHERE key = ?), ?, ?)"; + + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + + if (![self bindStringToStatement:statement index:1 string:@RCNExperimentTableKeyActiveMetadata]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (![self bindStringToStatement:statement index:2 string:@RCNExperimentTableKeyActiveMetadata]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue fromSource:(RCNDBSource)source { RCN_MUST_NOT_BE_MAIN_THREAD(); @@ -827,12 +859,36 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { experimentMetadata = [[NSMutableDictionary alloc] init]; } + /// Load activated experiments payload and metadata. + NSMutableArray *activatedExperimentPayloads = + [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyActivePayload]; + if (!experimentPayloads) { + activatedExperimentPayloads = [[NSMutableArray alloc] init]; + } + + NSMutableDictionary *activatedExperimentMetadata; + NSMutableArray *activatedExperiments = + [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyActiveMetadata]; + // There should be only one entry for experiment metadata. + if (activatedExperiments.count > 0) { + NSError *error; + activatedExperimentMetadata = + [NSJSONSerialization JSONObjectWithData:activatedExperiments[0] + options:NSJSONReadingMutableContainers + error:&error]; + } + if (!activatedExperimentMetadata) { + activatedExperimentMetadata = [[NSMutableDictionary alloc] init]; + } + if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler( YES, @{ @RCNExperimentTableKeyPayload : [experimentPayloads copy], - @RCNExperimentTableKeyMetadata : [experimentMetadata copy] + @RCNExperimentTableKeyMetadata : [experimentMetadata copy], + @RCNExperimentTableKeyActivePayload : [activatedExperimentPayloads copy], + @RCNExperimentTableKeyActiveMetadata : [activatedExperimentMetadata copy] }); }); } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index d0ef6c39c87..76f348ba625 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -186,6 +186,8 @@ - (void)updateExperimentStartTime { - (void)updateActiveExperiments { /// Put current fetched experiment metadata into activated experiment DB. + _activatedExperimentMetadata[kExperimentMetadataKeyLastStartTime] = + _experimentMetadata[kExperimentMetadataKeyLastStartTime]; NSError *error; NSData *serializedExperimentMetadata = [NSJSONSerialization dataWithJSONObject:_experimentMetadata @@ -203,9 +205,12 @@ - (void)updateActiveExperiments { } /// Put current fetched experiment payloads into activated experiment DB. - for (NSData *data in _experimentPayloads) { + [_activatedExperimentPayloads removeAllObjects]; + [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload]; + for (NSData *experiment in _experimentPayloads) { + [_activatedExperimentPayloads addObject:experiment]; [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload - value:data + value:experiment completionHandler:nil]; } } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m index 49f6f97fe54..81042245719 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m @@ -504,6 +504,70 @@ - (void)testWriteAndLoadExperiments { [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; } +- (void)testWriteAndLoadActivatedExperiments { + XCTestExpectation *updateAndLoadExperimentExpectation = + [self expectationWithDescription:@"Update and load experiment in database successfully"]; + + NSError *error; + NSArray *payload2 = @[ @"ab", @"cd" ]; + NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload2 + options:NSJSONWritingPrettyPrinted + error:&error]; + NSDictionary *payload3 = + @{@"experiment_ID" : @"35667", @"experiment_activate_name" : @"activate_game"}; + NSData *payloadData3 = [NSJSONSerialization dataWithJSONObject:payload3 + options:NSJSONWritingPrettyPrinted + error:&error]; + NSArray *payloads = @[ [[NSData alloc] init], payloadData2, payloadData3 ]; + + RCNDBCompletion writePayloadCompletion = ^(BOOL success, NSDictionary *result) { + NSDictionary *metadata = + @{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"}; + XCTAssertTrue(success); + RCNDBCompletion writeMetadataCompletion = ^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) { + XCTAssertTrue(success); + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActivePayload]); + XCTAssertEqualObjects(payloads, experimentResults[@RCNExperimentTableKeyActivePayload]); + + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActiveMetadata]); + XCTAssertEqualWithAccuracy( + -11, + [experimentResults[@RCNExperimentTableKeyActiveMetadata][@"last_known_start_time"] + doubleValue], + 1.0); + XCTAssertEqualObjects( + @"wonderful", + experimentResults[@RCNExperimentTableKeyActiveMetadata][@"experiment_new_metadata"]); + [updateAndLoadExperimentExpectation fulfill]; + }; + [self->_DBManager loadExperimentWithCompletionHandler:readCompletion]; + }; + + NSError *error; + XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); + NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata + options:NSJSONWritingPrettyPrinted + error:&error]; + + [self->_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActiveMetadata + value:serializedMetadata + completionHandler:writeMetadataCompletion]; + }; + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload + value:[[NSData alloc] init] + completionHandler:nil]; + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload + value:payloadData2 + completionHandler:nil]; + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload + value:payloadData3 + completionHandler:writePayloadCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + - (void)testWriteAndLoadMetadataMultipleTimes { XCTestExpectation *updateAndLoadMetadataExpectation = [self expectationWithDescription:@"Update and load experiment metadata in database successfully"]; @@ -548,6 +612,52 @@ - (void)testWriteAndLoadMetadataMultipleTimes { [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; } +- (void)testWriteAndLoadActivatedMetadataMultipleTimes { + XCTestExpectation *updateAndLoadMetadataExpectation = + [self expectationWithDescription: + @"Update and load activated experiment metadata in database successfully"]; + + RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) { + XCTAssertTrue(success); + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActivePayload]); + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActiveMetadata]); + XCTAssertEqualWithAccuracy( + 12345678, + [experimentResults[@RCNExperimentTableKeyActiveMetadata][@"last_known_start_time"] + doubleValue], + 1.0); + XCTAssertEqualObjects( + @"wonderful", + experimentResults[@RCNExperimentTableKeyActiveMetadata][@"experiment_new_metadata"]); + + [updateAndLoadMetadataExpectation fulfill]; + }; + NSDictionary *metadata = + @{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"}; + NSError *error; + XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); + NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata + options:NSJSONWritingPrettyPrinted + error:&error]; + + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActiveMetadata + value:serializedMetadata + completionHandler:nil]; + + metadata = @{@"last_known_start_time" : @(12345678), @"experiment_new_metadata" : @"wonderful"}; + XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); + serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata + options:NSJSONWritingPrettyPrinted + error:&error]; + + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActiveMetadata + value:serializedMetadata + completionHandler:nil]; + [_DBManager loadExperimentWithCompletionHandler:readCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + - (void)testUpdateAndloadLastFetchStatus { XCTestExpectation *updateAndLoadMetadataExpectation = [self expectationWithDescription:@"Update and load last fetch status in database successfully."]; From 5867edb9947bae0567ad550759a1da1c38a00cf5 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Thu, 11 May 2023 19:12:25 -0700 Subject: [PATCH 06/28] Fix metadata naming --- FirebaseRemoteConfig/Sources/RCNConfigDBManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index 8539e70e3dd..2c79765c8d0 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -862,7 +862,7 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { /// Load activated experiments payload and metadata. NSMutableArray *activatedExperimentPayloads = [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyActivePayload]; - if (!experimentPayloads) { + if (!activatedExperimentPayloads) { activatedExperimentPayloads = [[NSMutableArray alloc] init]; } From c2b6097432a9dca2bc151739a0e0dc30a7a460dd Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Thu, 11 May 2023 19:13:59 -0700 Subject: [PATCH 07/28] add comment --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 76f348ba625..59af7d6b846 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -94,6 +94,7 @@ - (void)loadExperimentFromTable { strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy]; } + /// Load activated experiments payload and metadata. if (result[@RCNExperimentTableKeyActivePayload]) { [strongSelf->_activatedExperimentPayloads removeAllObjects]; for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) { From 937630dbdb6b980ee2f6fe8f5cdc1a9ed6cfcafe Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Wed, 17 May 2023 11:24:57 -0700 Subject: [PATCH 08/28] Don't save experiments metadata b/c it isn't required to diff experiments --- .../Sources/RCNConfigDBManager.m | 52 +----------- .../Sources/RCNConfigDefines.h | 1 - .../Sources/RCNConfigExperiment.m | 27 +------ .../Tests/Unit/RCNConfigDBManagerTest.m | 81 ++----------------- 4 files changed, 8 insertions(+), 153 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index 2c79765c8d0..dd7b86daf39 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -523,9 +523,6 @@ - (BOOL)insertExperimentTableWithKey:(NSString *)key value:(NSData *)dataValue { if ([key isEqualToString:@RCNExperimentTableKeyMetadata]) { return [self updateExperimentMetadata:dataValue]; } - if ([key isEqualToString:@RCNExperimentTableKeyActiveMetadata]) { - return [self updateActivatedExperimentMetadata:dataValue]; - } RCN_MUST_NOT_BE_MAIN_THREAD(); const char *SQL = "INSERT INTO " RCNTableNameExperiment " (key, value) values (?, ?)"; @@ -579,35 +576,6 @@ - (BOOL)updateExperimentMetadata:(NSData *)dataValue { return YES; } -- (BOOL)updateActivatedExperimentMetadata:(NSData *)dataValue { - RCN_MUST_NOT_BE_MAIN_THREAD(); - const char *SQL = "INSERT OR REPLACE INTO " RCNTableNameExperiment - " (_id, key, value) values ((SELECT _id from " RCNTableNameExperiment - " WHERE key = ?), ?, ?)"; - - sqlite3_stmt *statement = [self prepareSQL:SQL]; - if (!statement) { - return NO; - } - - if (![self bindStringToStatement:statement index:1 string:@RCNExperimentTableKeyActiveMetadata]) { - return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; - } - - if (![self bindStringToStatement:statement index:2 string:@RCNExperimentTableKeyActiveMetadata]) { - return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; - } - if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { - return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; - } - - if (sqlite3_step(statement) != SQLITE_DONE) { - return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; - } - sqlite3_finalize(statement); - return YES; -} - - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue fromSource:(RCNDBSource)source { RCN_MUST_NOT_BE_MAIN_THREAD(); @@ -859,36 +827,20 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { experimentMetadata = [[NSMutableDictionary alloc] init]; } - /// Load activated experiments payload and metadata. + /// Load activated experiments payload. NSMutableArray *activatedExperimentPayloads = [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyActivePayload]; if (!activatedExperimentPayloads) { activatedExperimentPayloads = [[NSMutableArray alloc] init]; } - NSMutableDictionary *activatedExperimentMetadata; - NSMutableArray *activatedExperiments = - [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyActiveMetadata]; - // There should be only one entry for experiment metadata. - if (activatedExperiments.count > 0) { - NSError *error; - activatedExperimentMetadata = - [NSJSONSerialization JSONObjectWithData:activatedExperiments[0] - options:NSJSONReadingMutableContainers - error:&error]; - } - if (!activatedExperimentMetadata) { - activatedExperimentMetadata = [[NSMutableDictionary alloc] init]; - } - if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler( YES, @{ @RCNExperimentTableKeyPayload : [experimentPayloads copy], @RCNExperimentTableKeyMetadata : [experimentMetadata copy], - @RCNExperimentTableKeyActivePayload : [activatedExperimentPayloads copy], - @RCNExperimentTableKeyActiveMetadata : [activatedExperimentMetadata copy] + @RCNExperimentTableKeyActivePayload : [activatedExperimentPayloads copy] }); }); } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h index c13fa5b51e8..cf08f738105 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h @@ -30,7 +30,6 @@ #define RCNExperimentTableKeyPayload "experiment_payload" #define RCNExperimentTableKeyMetadata "experiment_metadata" -#define RCNExperimentTableKeyActiveMetadata "experiment_active_metadata" #define RCNExperimentTableKeyActivePayload "experiment_active_payload" #endif diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 59af7d6b846..e587b451832 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -33,9 +33,7 @@ @interface RCNConfigExperiment () @property(nonatomic, strong) NSMutableDictionary *experimentMetadata; ///< Experiment metadata @property(nonatomic, strong) - NSMutableArray *activatedExperimentPayloads; ///< Activated experiment payloads. -@property(nonatomic, strong) NSMutableDictionary - *activatedExperimentMetadata; ///< Activated experiment metadata + NSMutableArray *activatedExperimentPayloads; ///< Activated experiment payloads. @property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. @property(nonatomic, strong) FIRExperimentController *experimentController; @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter; @@ -110,10 +108,6 @@ - (void)loadExperimentFromTable { } } } - if (result[@RCNExperimentTableKeyActiveMetadata]) { - strongSelf->_activatedExperimentMetadata = - [result[@RCNExperimentTableKeyActiveMetadata] mutableCopy]; - } }; [_DBManager loadExperimentWithCompletionHandler:completionHandler]; } @@ -186,25 +180,6 @@ - (void)updateExperimentStartTime { } - (void)updateActiveExperiments { - /// Put current fetched experiment metadata into activated experiment DB. - _activatedExperimentMetadata[kExperimentMetadataKeyLastStartTime] = - _experimentMetadata[kExperimentMetadataKeyLastStartTime]; - NSError *error; - NSData *serializedExperimentMetadata = - [NSJSONSerialization dataWithJSONObject:_experimentMetadata - options:NSJSONWritingPrettyPrinted - error:&error]; - - if (error) { - FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000092", - @"Invalid activated experiment metadata to be serialized."); - return; - } else { - [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActiveMetadata - value:serializedExperimentMetadata - completionHandler:nil]; - } - /// Put current fetched experiment payloads into activated experiment DB. [_activatedExperimentPayloads removeAllObjects]; [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m index 81042245719..23705be1abf 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m @@ -521,39 +521,14 @@ - (void)testWriteAndLoadActivatedExperiments { NSArray *payloads = @[ [[NSData alloc] init], payloadData2, payloadData3 ]; RCNDBCompletion writePayloadCompletion = ^(BOOL success, NSDictionary *result) { - NSDictionary *metadata = - @{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"}; XCTAssertTrue(success); - RCNDBCompletion writeMetadataCompletion = ^(BOOL success, NSDictionary *result) { + RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) { XCTAssertTrue(success); - RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) { - XCTAssertTrue(success); - XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActivePayload]); - XCTAssertEqualObjects(payloads, experimentResults[@RCNExperimentTableKeyActivePayload]); - - XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActiveMetadata]); - XCTAssertEqualWithAccuracy( - -11, - [experimentResults[@RCNExperimentTableKeyActiveMetadata][@"last_known_start_time"] - doubleValue], - 1.0); - XCTAssertEqualObjects( - @"wonderful", - experimentResults[@RCNExperimentTableKeyActiveMetadata][@"experiment_new_metadata"]); - [updateAndLoadExperimentExpectation fulfill]; - }; - [self->_DBManager loadExperimentWithCompletionHandler:readCompletion]; + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActivePayload]); + XCTAssertEqualObjects(payloads, experimentResults[@RCNExperimentTableKeyActivePayload]); + [updateAndLoadExperimentExpectation fulfill]; }; - - NSError *error; - XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); - NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata - options:NSJSONWritingPrettyPrinted - error:&error]; - - [self->_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActiveMetadata - value:serializedMetadata - completionHandler:writeMetadataCompletion]; + [self->_DBManager loadExperimentWithCompletionHandler:readCompletion]; }; [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload value:[[NSData alloc] init] @@ -612,52 +587,6 @@ - (void)testWriteAndLoadMetadataMultipleTimes { [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; } -- (void)testWriteAndLoadActivatedMetadataMultipleTimes { - XCTestExpectation *updateAndLoadMetadataExpectation = - [self expectationWithDescription: - @"Update and load activated experiment metadata in database successfully"]; - - RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) { - XCTAssertTrue(success); - XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActivePayload]); - XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyActiveMetadata]); - XCTAssertEqualWithAccuracy( - 12345678, - [experimentResults[@RCNExperimentTableKeyActiveMetadata][@"last_known_start_time"] - doubleValue], - 1.0); - XCTAssertEqualObjects( - @"wonderful", - experimentResults[@RCNExperimentTableKeyActiveMetadata][@"experiment_new_metadata"]); - - [updateAndLoadMetadataExpectation fulfill]; - }; - NSDictionary *metadata = - @{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"}; - NSError *error; - XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); - NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata - options:NSJSONWritingPrettyPrinted - error:&error]; - - [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActiveMetadata - value:serializedMetadata - completionHandler:nil]; - - metadata = @{@"last_known_start_time" : @(12345678), @"experiment_new_metadata" : @"wonderful"}; - XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); - serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata - options:NSJSONWritingPrettyPrinted - error:&error]; - - [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActiveMetadata - value:serializedMetadata - completionHandler:nil]; - [_DBManager loadExperimentWithCompletionHandler:readCompletion]; - - [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; -} - - (void)testUpdateAndloadLastFetchStatus { XCTestExpectation *updateAndLoadMetadataExpectation = [self expectationWithDescription:@"Update and load last fetch status in database successfully."]; From d49bffba13e6c770748a5813078b8c7109ddc545 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Wed, 17 May 2023 12:03:05 -0700 Subject: [PATCH 09/28] Create diff method --- .../Sources/RCNConfigExperiment.m | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index e587b451832..861ce618025 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -27,6 +27,8 @@ static NSString *const kMethodNameLatestStartTime = @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:"; +static NSString *const kExperimentIdKey = @"experimentId"; + @interface RCNConfigExperiment () @property(nonatomic, strong) NSMutableArray *experimentPayloads; ///< Experiment payloads. @@ -196,4 +198,38 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime andPayloads:_experimentPayloads]; } + +- (NSMutableSet *)getChangedABTExperiments { + NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + + NSMutableDictionary *fetchedExperiments = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *activeExperiments = [[NSMutableDictionary alloc] init]; + NSMutableSet *allExperimentIds = [[NSMutableSet alloc] init]; + + for (NSData *experiment in _experimentPayloads) { + NSError *error; + NSDictionary *experimentJSON = [NSJSONSerialization JSONObjectWithData:experiment + options:NSJSONReadingMutableContainers + error:&error]; + if (!error) { + [fetchedExperiments setObject: experimentJSON forKey:[experimentJSON valueForKey:kExperimentIdKey]]; + } + } + [allExperimentIds addObjectsFromArray:[fetchedExperiments allKeys]]; + + for (NSData *experiment in _activatedExperimentPayloads) { + NSError *error; + NSDictionary *experimentJSON = [NSJSONSerialization JSONObjectWithData:experiment + options:NSJSONReadingMutableContainers + error:&error]; + if (!error) { + [activeExperiments setObject: experimentJSON forKey:[experimentJSON valueForKey:kExperimentIdKey]]; + } + } + [allExperimentIds addObjectsFromArray:[activeExperiments allKeys]]; + + + + return changedKeys; +} @end From 00675486e6f20af5a1234b7ad52026f3817dbe42 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Wed, 17 May 2023 14:13:33 -0700 Subject: [PATCH 10/28] Adding diffing logic between fetched and activated experiments --- .../Sources/RCNConfigExperiment.m | 105 +++++++++++++++--- 1 file changed, 87 insertions(+), 18 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 861ce618025..86816e45bfd 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -28,6 +28,7 @@ @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:"; static NSString *const kExperimentIdKey = @"experimentId"; +static NSString *const kAffectedParameterKeys = @"affectedParameterKeys"; @interface RCNConfigExperiment () @property(nonatomic, strong) @@ -199,36 +200,104 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist andPayloads:_experimentPayloads]; } -- (NSMutableSet *)getChangedABTExperiments { - NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; +- (NSMutableDictionary *)getExperimentsMap:(NSMutableArray *)experiments { + NSMutableDictionary *experimentsMap = [[NSMutableDictionary alloc] init]; - NSMutableDictionary *fetchedExperiments = [[NSMutableDictionary alloc] init]; - NSMutableDictionary *activeExperiments = [[NSMutableDictionary alloc] init]; - NSMutableSet *allExperimentIds = [[NSMutableSet alloc] init]; - - for (NSData *experiment in _experimentPayloads) { + for (NSData *experiment in experiments) { NSError *error; NSDictionary *experimentJSON = [NSJSONSerialization JSONObjectWithData:experiment options:NSJSONReadingMutableContainers error:&error]; - if (!error) { - [fetchedExperiments setObject: experimentJSON forKey:[experimentJSON valueForKey:kExperimentIdKey]]; + if (!error && experimentJSON) { + [experimentsMap setObject: experimentJSON forKey:[experimentJSON valueForKey:kExperimentIdKey]]; } } - [allExperimentIds addObjectsFromArray:[fetchedExperiments allKeys]]; - for (NSData *experiment in _activatedExperimentPayloads) { - NSError *error; - NSDictionary *experimentJSON = [NSJSONSerialization JSONObjectWithData:experiment - options:NSJSONReadingMutableContainers - error:&error]; - if (!error) { - [activeExperiments setObject: experimentJSON forKey:[experimentJSON valueForKey:kExperimentIdKey]]; + return experimentsMap; +} + +- (bool)isExperimentMetadataSame: (NSDictionary *)activeExperiment fetchedExperiment:(NSDictionary *)fetchedExperiment { + NSMutableDictionary *activeExperimentCopy = [activeExperiment mutableCopy]; + NSMutableDictionary *fetchedExperimentCopy = [fetchedExperiment mutableCopy]; + + if ([activeExperimentCopy objectForKey:kAffectedParameterKeys]) { + [activeExperimentCopy removeObjectForKey:kAffectedParameterKeys]; + } + if ([fetchedExperimentCopy objectForKey:kAffectedParameterKeys]) { + [fetchedExperimentCopy removeObjectForKey:kAffectedParameterKeys]; + } + + + return [activeExperimentCopy isEqualToDictionary:fetchedExperimentCopy]; +} + +- (NSMutableSet *) compareExperimentConfigKeys: (NSMutableArray *)activeExperimentKeys fetchedExperimentKeys:(NSMutableArray *)fetchedExperimentKeys { + NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + NSMutableSet *activeKeys = [[NSMutableSet alloc] init]; + NSMutableSet *fetchedKeys = [[NSMutableSet alloc] init]; + + [activeKeys addObjectsFromArray:activeExperimentKeys]; + [fetchedKeys addObjectsFromArray:fetchedExperimentKeys]; + changedKeys = [[changedKeys setByAddingObjectsFromSet:activeKeys] mutableCopy]; + changedKeys = [[changedKeys setByAddingObjectsFromSet:fetchedKeys] mutableCopy]; + + NSSet *allKeys = [[NSSet alloc] init]; + allKeys = [allKeys setByAddingObjectsFromSet:changedKeys]; + + for (NSString *key in allKeys) { + if ([activeKeys valueForKey:key] && [fetchedKeys valueForKey:key]) { + [changedKeys removeObject:key]; } } - [allExperimentIds addObjectsFromArray:[activeExperiments allKeys]]; + return changedKeys; +} + +- (NSMutableSet *)getChangedABTExperiments { + NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + + NSMutableDictionary *activeExperiments = [self getExperimentsMap:_activatedExperimentPayloads]; + NSMutableDictionary *fetchedExperiments = [self getExperimentsMap:_experimentPayloads]; + NSMutableSet *allExperimentIds = [[NSMutableSet alloc] init]; + [allExperimentIds addObjectsFromArray:[fetchedExperiments allKeys]]; + [allExperimentIds addObjectsFromArray:[activeExperiments allKeys]]; + + for (NSString *experimentId in allExperimentIds) { + if (![activeExperiments objectForKey:experimentId] || ![fetchedExperiments objectForKey:experimentId]) { + NSDictionary *experiment; + if ([activeExperiments objectForKey:experimentId]) { + experiment = [activeExperiments objectForKey:experimentId]; + } else { + experiment = [fetchedExperiments objectForKey:experimentId]; + } + + if ([experiment objectForKey:kAffectedParameterKeys]) { + NSMutableArray *keys = [experiment objectForKey:kAffectedParameterKeys]; + [changedKeys addObjectsFromArray:keys]; + } + } else { + NSDictionary *activeExperiement = [activeExperiments objectForKey:experimentId]; + NSDictionary *fetchedExperiment = [fetchedExperiments objectForKey:experimentId]; + + NSMutableArray *activeExperimentKeys = [[NSMutableArray alloc] init]; + NSMutableArray *fetchedExperimentKeys = [[NSMutableArray alloc] init]; + + if ([activeExperiments objectForKey:kAffectedParameterKeys]) { + activeExperimentKeys = (NSMutableArray *)[activeExperiments objectForKey:kAffectedParameterKeys]; + } + if ([fetchedExperiments objectForKey:kAffectedParameterKeys]) { + fetchedExperimentKeys = (NSMutableArray *)[fetchedExperiments objectForKey:kAffectedParameterKeys]; + } + + if ([self isExperimentMetadataSame:activeExperiement fetchedExperiment:fetchedExperiment]) { + [changedKeys addObjectsFromArray:activeExperimentKeys]; + [changedKeys addObjectsFromArray:fetchedExperimentKeys]; + } else { + return [self compareExperimentConfigKeys:activeExperimentKeys fetchedExperimentKeys:fetchedExperimentKeys]; + } + } + } return changedKeys; } From d02b9e5004a956c197e6521aaa35597798976ede Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Wed, 17 May 2023 14:14:15 -0700 Subject: [PATCH 11/28] formatting --- .../Sources/RCNConfigExperiment.m | 191 +++++++++--------- 1 file changed, 101 insertions(+), 90 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 86816e45bfd..1c88ec1e833 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -200,105 +200,116 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist andPayloads:_experimentPayloads]; } -- (NSMutableDictionary *)getExperimentsMap:(NSMutableArray *)experiments { - NSMutableDictionary *experimentsMap = [[NSMutableDictionary alloc] init]; - - for (NSData *experiment in experiments) { - NSError *error; - NSDictionary *experimentJSON = [NSJSONSerialization JSONObjectWithData:experiment - options:NSJSONReadingMutableContainers - error:&error]; - if (!error && experimentJSON) { - [experimentsMap setObject: experimentJSON forKey:[experimentJSON valueForKey:kExperimentIdKey]]; - } +- (NSMutableDictionary *)getExperimentsMap: + (NSMutableArray *)experiments { + NSMutableDictionary *experimentsMap = + [[NSMutableDictionary alloc] init]; + + for (NSData *experiment in experiments) { + NSError *error; + NSDictionary *experimentJSON = + [NSJSONSerialization JSONObjectWithData:experiment + options:NSJSONReadingMutableContainers + error:&error]; + if (!error && experimentJSON) { + [experimentsMap setObject:experimentJSON + forKey:[experimentJSON valueForKey:kExperimentIdKey]]; } - - return experimentsMap; + } + + return experimentsMap; } -- (bool)isExperimentMetadataSame: (NSDictionary *)activeExperiment fetchedExperiment:(NSDictionary *)fetchedExperiment { - NSMutableDictionary *activeExperimentCopy = [activeExperiment mutableCopy]; - NSMutableDictionary *fetchedExperimentCopy = [fetchedExperiment mutableCopy]; - - if ([activeExperimentCopy objectForKey:kAffectedParameterKeys]) { - [activeExperimentCopy removeObjectForKey:kAffectedParameterKeys]; - } - if ([fetchedExperimentCopy objectForKey:kAffectedParameterKeys]) { - [fetchedExperimentCopy removeObjectForKey:kAffectedParameterKeys]; - } - - - return [activeExperimentCopy isEqualToDictionary:fetchedExperimentCopy]; +- (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment + fetchedExperiment:(NSDictionary *)fetchedExperiment { + NSMutableDictionary *activeExperimentCopy = [activeExperiment mutableCopy]; + NSMutableDictionary *fetchedExperimentCopy = [fetchedExperiment mutableCopy]; + + if ([activeExperimentCopy objectForKey:kAffectedParameterKeys]) { + [activeExperimentCopy removeObjectForKey:kAffectedParameterKeys]; + } + if ([fetchedExperimentCopy objectForKey:kAffectedParameterKeys]) { + [fetchedExperimentCopy removeObjectForKey:kAffectedParameterKeys]; + } + + return [activeExperimentCopy isEqualToDictionary:fetchedExperimentCopy]; } -- (NSMutableSet *) compareExperimentConfigKeys: (NSMutableArray *)activeExperimentKeys fetchedExperimentKeys:(NSMutableArray *)fetchedExperimentKeys { - NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; - NSMutableSet *activeKeys = [[NSMutableSet alloc] init]; - NSMutableSet *fetchedKeys = [[NSMutableSet alloc] init]; - - [activeKeys addObjectsFromArray:activeExperimentKeys]; - [fetchedKeys addObjectsFromArray:fetchedExperimentKeys]; - changedKeys = [[changedKeys setByAddingObjectsFromSet:activeKeys] mutableCopy]; - changedKeys = [[changedKeys setByAddingObjectsFromSet:fetchedKeys] mutableCopy]; - - NSSet *allKeys = [[NSSet alloc] init]; - allKeys = [allKeys setByAddingObjectsFromSet:changedKeys]; - - for (NSString *key in allKeys) { - if ([activeKeys valueForKey:key] && [fetchedKeys valueForKey:key]) { - [changedKeys removeObject:key]; - } +- (NSMutableSet *)compareExperimentConfigKeys:(NSMutableArray *)activeExperimentKeys + fetchedExperimentKeys:(NSMutableArray *)fetchedExperimentKeys { + NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + NSMutableSet *activeKeys = [[NSMutableSet alloc] init]; + NSMutableSet *fetchedKeys = [[NSMutableSet alloc] init]; + + [activeKeys addObjectsFromArray:activeExperimentKeys]; + [fetchedKeys addObjectsFromArray:fetchedExperimentKeys]; + changedKeys = [[changedKeys setByAddingObjectsFromSet:activeKeys] mutableCopy]; + changedKeys = [[changedKeys setByAddingObjectsFromSet:fetchedKeys] mutableCopy]; + + NSSet *allKeys = [[NSSet alloc] init]; + allKeys = [allKeys setByAddingObjectsFromSet:changedKeys]; + + for (NSString *key in allKeys) { + if ([activeKeys valueForKey:key] && [fetchedKeys valueForKey:key]) { + [changedKeys removeObject:key]; } - - return changedKeys; + } + + return changedKeys; } - (NSMutableSet *)getChangedABTExperiments { - NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; - - NSMutableDictionary *activeExperiments = [self getExperimentsMap:_activatedExperimentPayloads]; - NSMutableDictionary *fetchedExperiments = [self getExperimentsMap:_experimentPayloads]; - - NSMutableSet *allExperimentIds = [[NSMutableSet alloc] init]; - [allExperimentIds addObjectsFromArray:[fetchedExperiments allKeys]]; - [allExperimentIds addObjectsFromArray:[activeExperiments allKeys]]; - - for (NSString *experimentId in allExperimentIds) { - if (![activeExperiments objectForKey:experimentId] || ![fetchedExperiments objectForKey:experimentId]) { - NSDictionary *experiment; - if ([activeExperiments objectForKey:experimentId]) { - experiment = [activeExperiments objectForKey:experimentId]; - } else { - experiment = [fetchedExperiments objectForKey:experimentId]; - } - - if ([experiment objectForKey:kAffectedParameterKeys]) { - NSMutableArray *keys = [experiment objectForKey:kAffectedParameterKeys]; - [changedKeys addObjectsFromArray:keys]; - } - } else { - NSDictionary *activeExperiement = [activeExperiments objectForKey:experimentId]; - NSDictionary *fetchedExperiment = [fetchedExperiments objectForKey:experimentId]; - - NSMutableArray *activeExperimentKeys = [[NSMutableArray alloc] init]; - NSMutableArray *fetchedExperimentKeys = [[NSMutableArray alloc] init]; - - if ([activeExperiments objectForKey:kAffectedParameterKeys]) { - activeExperimentKeys = (NSMutableArray *)[activeExperiments objectForKey:kAffectedParameterKeys]; - } - if ([fetchedExperiments objectForKey:kAffectedParameterKeys]) { - fetchedExperimentKeys = (NSMutableArray *)[fetchedExperiments objectForKey:kAffectedParameterKeys]; - } - - if ([self isExperimentMetadataSame:activeExperiement fetchedExperiment:fetchedExperiment]) { - [changedKeys addObjectsFromArray:activeExperimentKeys]; - [changedKeys addObjectsFromArray:fetchedExperimentKeys]; - } else { - return [self compareExperimentConfigKeys:activeExperimentKeys fetchedExperimentKeys:fetchedExperimentKeys]; - } - } + NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + + NSMutableDictionary *activeExperiments = + [self getExperimentsMap:_activatedExperimentPayloads]; + NSMutableDictionary *fetchedExperiments = + [self getExperimentsMap:_experimentPayloads]; + + NSMutableSet *allExperimentIds = [[NSMutableSet alloc] init]; + [allExperimentIds addObjectsFromArray:[fetchedExperiments allKeys]]; + [allExperimentIds addObjectsFromArray:[activeExperiments allKeys]]; + + for (NSString *experimentId in allExperimentIds) { + if (![activeExperiments objectForKey:experimentId] || + ![fetchedExperiments objectForKey:experimentId]) { + NSDictionary *experiment; + if ([activeExperiments objectForKey:experimentId]) { + experiment = [activeExperiments objectForKey:experimentId]; + } else { + experiment = [fetchedExperiments objectForKey:experimentId]; + } + + if ([experiment objectForKey:kAffectedParameterKeys]) { + NSMutableArray *keys = [experiment objectForKey:kAffectedParameterKeys]; + [changedKeys addObjectsFromArray:keys]; + } + } else { + NSDictionary *activeExperiement = [activeExperiments objectForKey:experimentId]; + NSDictionary *fetchedExperiment = [fetchedExperiments objectForKey:experimentId]; + + NSMutableArray *activeExperimentKeys = [[NSMutableArray alloc] init]; + NSMutableArray *fetchedExperimentKeys = [[NSMutableArray alloc] init]; + + if ([activeExperiments objectForKey:kAffectedParameterKeys]) { + activeExperimentKeys = + (NSMutableArray *)[activeExperiments objectForKey:kAffectedParameterKeys]; + } + if ([fetchedExperiments objectForKey:kAffectedParameterKeys]) { + fetchedExperimentKeys = + (NSMutableArray *)[fetchedExperiments objectForKey:kAffectedParameterKeys]; + } + + if ([self isExperimentMetadataSame:activeExperiement fetchedExperiment:fetchedExperiment]) { + [changedKeys addObjectsFromArray:activeExperimentKeys]; + [changedKeys addObjectsFromArray:fetchedExperimentKeys]; + } else { + return [self compareExperimentConfigKeys:activeExperimentKeys + fetchedExperimentKeys:fetchedExperimentKeys]; + } } - - return changedKeys; + } + + return changedKeys; } @end From c9acee29e9511c9ab819162a802117d8e41dd12d Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Wed, 17 May 2023 15:14:39 -0700 Subject: [PATCH 12/28] Refactor more of main diffing method --- .../Sources/RCNConfigExperiment.m | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 1c88ec1e833..8fd30fd143d 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -220,6 +220,14 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist return experimentsMap; } +- (NSMutableArray *)extractConfigKeysFromExperiment:(NSDictionary *)experiment { + if (![experiment objectForKey:kAffectedParameterKeys]) { + return [[NSMutableArray alloc] init]; + } + + return (NSMutableArray *)[experiment objectForKey:kAffectedParameterKeys]; +} + - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment fetchedExperiment:(NSDictionary *)fetchedExperiment { NSMutableDictionary *activeExperimentCopy = [activeExperiment mutableCopy]; @@ -235,8 +243,9 @@ - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment return [activeExperimentCopy isEqualToDictionary:fetchedExperimentCopy]; } -- (NSMutableSet *)compareExperimentConfigKeys:(NSMutableArray *)activeExperimentKeys - fetchedExperimentKeys:(NSMutableArray *)fetchedExperimentKeys { +- (NSMutableSet *)getChangedExperimentConfigKeys:(NSMutableArray *)activeExperimentKeys + fetchedExperimentKeys: + (NSMutableArray *)fetchedExperimentKeys { NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; NSMutableSet *activeKeys = [[NSMutableSet alloc] init]; NSMutableSet *fetchedKeys = [[NSMutableSet alloc] init]; @@ -280,32 +289,24 @@ - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment experiment = [fetchedExperiments objectForKey:experimentId]; } - if ([experiment objectForKey:kAffectedParameterKeys]) { - NSMutableArray *keys = [experiment objectForKey:kAffectedParameterKeys]; - [changedKeys addObjectsFromArray:keys]; - } + [changedKeys addObjectsFromArray:[self extractConfigKeysFromExperiment:experiment]]; } else { - NSDictionary *activeExperiement = [activeExperiments objectForKey:experimentId]; + NSDictionary *activeExperiment = [activeExperiments objectForKey:experimentId]; NSDictionary *fetchedExperiment = [fetchedExperiments objectForKey:experimentId]; - NSMutableArray *activeExperimentKeys = [[NSMutableArray alloc] init]; - NSMutableArray *fetchedExperimentKeys = [[NSMutableArray alloc] init]; - - if ([activeExperiments objectForKey:kAffectedParameterKeys]) { - activeExperimentKeys = - (NSMutableArray *)[activeExperiments objectForKey:kAffectedParameterKeys]; - } - if ([fetchedExperiments objectForKey:kAffectedParameterKeys]) { - fetchedExperimentKeys = - (NSMutableArray *)[fetchedExperiments objectForKey:kAffectedParameterKeys]; - } + NSMutableArray *activeExperimentKeys = + [self extractConfigKeysFromExperiment:activeExperiment]; + NSMutableArray *fetchedExperimentKeys = + [self extractConfigKeysFromExperiment:fetchedExperiment]; - if ([self isExperimentMetadataSame:activeExperiement fetchedExperiment:fetchedExperiment]) { + if ([self isExperimentMetadataSame:activeExperiment fetchedExperiment:fetchedExperiment]) { [changedKeys addObjectsFromArray:activeExperimentKeys]; [changedKeys addObjectsFromArray:fetchedExperimentKeys]; } else { - return [self compareExperimentConfigKeys:activeExperimentKeys - fetchedExperimentKeys:fetchedExperimentKeys]; + changedKeys = [[changedKeys + setByAddingObjectsFromSet:[self getChangedExperimentConfigKeys:activeExperimentKeys + fetchedExperimentKeys:fetchedExperimentKeys]] + mutableCopy]; } } } From 8337c60cc3233ea6382fe2361e53eaa98e790863 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Wed, 17 May 2023 15:43:08 -0700 Subject: [PATCH 13/28] use ABT diffing in ConfigUpdate logic --- FirebaseRemoteConfig/Sources/RCNConfigContent.h | 4 +++- FirebaseRemoteConfig/Sources/RCNConfigContent.m | 7 ++++--- .../Sources/RCNConfigExperiment.h | 3 +++ FirebaseRemoteConfig/Sources/RCNConfigFetch.m | 15 +++++++++------ .../Tests/Unit/RCNConfigContentTest.m | 12 ++++++------ 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index 34d0895243a..1c2a141cbc9 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -66,6 +66,8 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; /// Returns the updated parameters between fetched and active config. -- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace; +- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace + withExperimentChanges: + (NSMutableSet *)changedExperimentKeys; @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 4f55a2e9274..882c10f967a 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -399,11 +399,12 @@ - (BOOL)checkAndWaitForInitialDatabaseLoad { } // Compare fetched config with active config and output what has changed -- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace { - // TODO: handle diff in experiment metadata - +- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace + withExperimentChanges: + (NSMutableSet *)changedExperimentKeys { FIRRemoteConfigUpdate *configUpdate; NSMutableSet *updatedKeys = [[NSMutableSet alloc] init]; + updatedKeys = [[updatedKeys setByAddingObjectsFromSet:changedExperimentKeys] mutableCopy]; NSDictionary *fetchedConfig = _fetchedConfig[FIRNamespace] ? _fetchedConfig[FIRNamespace] : [[NSDictionary alloc] init]; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h index 79051faa87b..f40fa6a1127 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h @@ -35,4 +35,7 @@ /// Update experiments to Firebase Analytics when `activateWithCompletion:` happens. - (void)updateExperimentsWithHandler:(nullable void (^)(NSError *_Nullable error))handler; + +/// Return config keys from experiments that have changed. +- (NSMutableSet *_Nonnull)getChangedABTExperiments; @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index c350ed3e086..71af366a299 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -216,8 +216,9 @@ - (void)realtimeFetchConfigWithNoExpirationDuration:(NSInteger)fetchAttemptNumbe if (strongSelf->_settings.lastFetchTimeInterval > 0) { FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000052", @"A fetch is already in progress. Using previous fetch results."); - FIRRemoteConfigUpdate *update = - [self->_content getConfigUpdateForNamespace:self->_FIRNamespace]; + FIRRemoteConfigUpdate *update = [self->_content + getConfigUpdateForNamespace:self->_FIRNamespace + withExperimentChanges:[self->_experiment getChangedABTExperiments]]; return [strongSelf reportCompletionWithStatus:strongSelf->_settings.lastFetchStatus withUpdate:update withError:nil @@ -537,8 +538,9 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties if (!data) { FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000043", @"RCN Fetch: No data in fetch response"); // There may still be a difference between fetched and active config - FIRRemoteConfigUpdate *update = - [strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace]; + FIRRemoteConfigUpdate *update = [strongSelf->_content + getConfigUpdateForNamespace:strongSelf->_FIRNamespace + withExperimentChanges:[self->_experiment getChangedABTExperiments]]; return [strongSelf reportCompletionWithStatus:FIRRemoteConfigFetchStatusSuccess withUpdate:update withError:nil @@ -615,8 +617,9 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties strongSelf->_settings.lastETag = latestETag; } // Compute config update after successful fetch - FIRRemoteConfigUpdate *update = - [strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace]; + FIRRemoteConfigUpdate *update = [strongSelf->_content + getConfigUpdateForNamespace:strongSelf->_FIRNamespace + withExperimentChanges:[self->_experiment getChangedABTExperiments]]; [strongSelf->_settings updateMetadataWithFetchSuccessStatus:YES diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index d4f33bf0f71..dc2adf54fa9 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -344,7 +344,7 @@ - (void)testConfigUpdate_noChange_emptyResponse { toSource:RCNDBSourceActive forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 0); } @@ -368,7 +368,7 @@ - (void)testConfigUpdate_paramAdded_returnsNewKey { p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:newParam]); @@ -394,7 +394,7 @@ - (void)testConfigUpdate_paramValueChanged_returnsUpdatedKey { [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); @@ -420,7 +420,7 @@ - (void)testConfigUpdate_paramDeleted_returnsDeletedKey { [self createFetchResponseWithConfigEntries:@{newParam : value1} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 2); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); // deleted @@ -455,7 +455,7 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { forKey:RCNFetchResponseKeyPersonalizationMetadata]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); @@ -480,7 +480,7 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { [self createFetchResponseWithConfigEntries:@{existingParam : value1} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); From 12be9c76f6ccf9d3f0630706954a4ba9380d47f5 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Wed, 17 May 2023 15:58:13 -0700 Subject: [PATCH 14/28] Add extra assertion for test --- FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m index 78bd7c06ca8..268f8758a7d 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -38,6 +38,7 @@ - (instancetype)initWithAnalytics:(nullable id)analytics; @interface RCNConfigExperiment () @property(nonatomic, copy) NSMutableArray *experimentPayloads; @property(nonatomic, copy) NSMutableDictionary *experimentMetadata; +@property(nonatomic, copy) NSMutableArray *activatedExperimentPayloads; @property(nonatomic, strong) RCNConfigDBManager *DBManager; - (NSTimeInterval)updateExperimentStartTime; - (void)loadExperimentFromTable; @@ -234,6 +235,7 @@ - (void)testUpdateExperiments { XCTAssertEqualObjects(experiment.experimentMetadata[@"last_experiment_start_time"], @(12345678)); OCMVerify([experiment updateActiveExperiments]); + XCTAssertEqualObjects(experiment.activatedExperimentPayloads, @[ payloadData ]); }]; } From 92efb59bd757e9b47a1c728c2a9fa743e64dabc0 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Thu, 18 May 2023 14:52:05 -0700 Subject: [PATCH 15/28] Add tests --- .../Sources/RCNConfigExperiment.h | 2 +- .../Sources/RCNConfigExperiment.m | 9 +- FirebaseRemoteConfig/Sources/RCNConfigFetch.m | 6 +- .../Tests/Unit/RCNConfigContentTest.m | 50 +++++++- .../Tests/Unit/RCNConfigExperimentTest.m | 114 ++++++++++++++++++ .../Tests/Unit/TestABTPayload.txt | 3 +- 6 files changed, 168 insertions(+), 16 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h index f40fa6a1127..de74fd196ed 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h @@ -37,5 +37,5 @@ - (void)updateExperimentsWithHandler:(nullable void (^)(NSError *_Nullable error))handler; /// Return config keys from experiments that have changed. -- (NSMutableSet *_Nonnull)getChangedABTExperiments; +- (NSMutableSet *_Nonnull)getKeysAffectedByChangedExperiments; @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 8fd30fd143d..63d709c3960 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -255,11 +255,10 @@ - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment changedKeys = [[changedKeys setByAddingObjectsFromSet:activeKeys] mutableCopy]; changedKeys = [[changedKeys setByAddingObjectsFromSet:fetchedKeys] mutableCopy]; - NSSet *allKeys = [[NSSet alloc] init]; - allKeys = [allKeys setByAddingObjectsFromSet:changedKeys]; + NSSet *allKeys = [changedKeys copy]; for (NSString *key in allKeys) { - if ([activeKeys valueForKey:key] && [fetchedKeys valueForKey:key]) { + if ([activeKeys containsObject:key] && [fetchedKeys containsObject:key]) { [changedKeys removeObject:key]; } } @@ -267,7 +266,7 @@ - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment return changedKeys; } -- (NSMutableSet *)getChangedABTExperiments { +- (NSMutableSet *)getKeysAffectedByChangedExperiments { NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; NSMutableDictionary *activeExperiments = @@ -299,7 +298,7 @@ - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment NSMutableArray *fetchedExperimentKeys = [self extractConfigKeysFromExperiment:fetchedExperiment]; - if ([self isExperimentMetadataSame:activeExperiment fetchedExperiment:fetchedExperiment]) { + if (![self isExperimentMetadataSame:activeExperiment fetchedExperiment:fetchedExperiment]) { [changedKeys addObjectsFromArray:activeExperimentKeys]; [changedKeys addObjectsFromArray:fetchedExperimentKeys]; } else { diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index 71af366a299..95977fbdb9f 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -218,7 +218,7 @@ - (void)realtimeFetchConfigWithNoExpirationDuration:(NSInteger)fetchAttemptNumbe @"A fetch is already in progress. Using previous fetch results."); FIRRemoteConfigUpdate *update = [self->_content getConfigUpdateForNamespace:self->_FIRNamespace - withExperimentChanges:[self->_experiment getChangedABTExperiments]]; + withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]]; return [strongSelf reportCompletionWithStatus:strongSelf->_settings.lastFetchStatus withUpdate:update withError:nil @@ -540,7 +540,7 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties // There may still be a difference between fetched and active config FIRRemoteConfigUpdate *update = [strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace - withExperimentChanges:[self->_experiment getChangedABTExperiments]]; + withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]]; return [strongSelf reportCompletionWithStatus:FIRRemoteConfigFetchStatusSuccess withUpdate:update withError:nil @@ -619,7 +619,7 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties // Compute config update after successful fetch FIRRemoteConfigUpdate *update = [strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace - withExperimentChanges:[self->_experiment getChangedABTExperiments]]; + withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]]; [strongSelf->_settings updateMetadataWithFetchSuccessStatus:YES diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index dc2adf54fa9..37fc97fe073 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -344,11 +344,39 @@ - (void)testConfigUpdate_noChange_emptyResponse { toSource:RCNDBSourceActive forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = + [_configContent getConfigUpdateForNamespace:namespace + withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 0); } +- (void)testConfigUpdate_noParamChange_butExperimentChange { + NSString *namespace = @"test_namespace"; + + // populate fetched config + NSMutableDictionary *fetchResponse = + [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil]; + [_configContent 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}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + + NSMutableSet *experimentKeys = [[NSMutableSet alloc] init]; + [experimentKeys addObject:@"key_2"]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace + withExperimentChanges:experimentKeys]; + + XCTAssertTrue([update updatedKeys].count == 1); + XCTAssertTrue([[update updatedKeys] containsObject:@"key_2"]); +} + - (void)testConfigUpdate_paramAdded_returnsNewKey { NSString *namespace = @"test_namespace"; NSString *newParam = @"key2"; @@ -368,7 +396,9 @@ - (void)testConfigUpdate_paramAdded_returnsNewKey { p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = + [_configContent getConfigUpdateForNamespace:namespace + withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:newParam]); @@ -394,7 +424,9 @@ - (void)testConfigUpdate_paramValueChanged_returnsUpdatedKey { [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = + [_configContent getConfigUpdateForNamespace:namespace + withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); @@ -420,7 +452,9 @@ - (void)testConfigUpdate_paramDeleted_returnsDeletedKey { [self createFetchResponseWithConfigEntries:@{newParam : value1} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = + [_configContent getConfigUpdateForNamespace:namespace + withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 2); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); // deleted @@ -455,7 +489,9 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { forKey:RCNFetchResponseKeyPersonalizationMetadata]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = + [_configContent getConfigUpdateForNamespace:namespace + withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); @@ -480,7 +516,9 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { [self createFetchResponseWithConfigEntries:@{existingParam : value1} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = + [_configContent getConfigUpdateForNamespace:namespace + withExperimentChanges:[[NSMutableSet alloc] init]]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m index 268f8758a7d..1bd15cb4328 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -43,6 +43,7 @@ @interface RCNConfigExperiment () - (NSTimeInterval)updateExperimentStartTime; - (void)loadExperimentFromTable; - (void)updateActiveExperiments; +- (NSMutableSet *_Nonnull)getKeysAffectedByChangedExperiments; @end @interface RCNConfigExperimentTest : XCTestCase { @@ -239,6 +240,119 @@ - (void)testUpdateExperiments { }]; } +- (void)testExperimentDiff_addedExperiment { + FIRExperimentController *experimentController = + [[FIRExperimentController alloc] initWithAnalytics:nil]; + id mockExperimentController = OCMPartialMock(experimentController); + RCNConfigExperiment *experiment = + [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock + experimentController:mockExperimentController]; + + NSData *payloadData1 = [[self class] payloadDataFromTestFile]; + experiment.activatedExperimentPayloads = [@[ 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]; + experiment.experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy]; + + NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; + XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); +} + +- (void)testExperimentDiff_changedExperimentMetadata { + FIRExperimentController *experimentController = + [[FIRExperimentController alloc] initWithAnalytics:nil]; + id mockExperimentController = OCMPartialMock(experimentController); + RCNConfigExperiment *experiment = + [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock + experimentController:mockExperimentController]; + + NSData *payloadData1 = [[self class] payloadDataFromTestFile]; + experiment.activatedExperimentPayloads = [@[ 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]; + experiment.experimentPayloads = [@[ payloadData2 ] mutableCopy]; + + NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; + XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); +} + +- (void)testExperimentDiff_changedExperimentKeys { + FIRExperimentController *experimentController = + [[FIRExperimentController alloc] initWithAnalytics:nil]; + id mockExperimentController = OCMPartialMock(experimentController); + RCNConfigExperiment *experiment = + [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock + experimentController:mockExperimentController]; + + NSData *payloadData1 = [[self class] payloadDataFromTestFile]; + experiment.activatedExperimentPayloads = [@[ 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]; + experiment.experimentPayloads = [@[ payloadData2 ] mutableCopy]; + + NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; + XCTAssertTrue([changedKeys containsObject:@"test_key_2"]); +} + +- (void)testExperimentDiff_deletedExperiment { + FIRExperimentController *experimentController = + [[FIRExperimentController alloc] initWithAnalytics:nil]; + id mockExperimentController = OCMPartialMock(experimentController); + RCNConfigExperiment *experiment = + [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock + experimentController:mockExperimentController]; + + NSData *payloadData1 = [[self class] payloadDataFromTestFile]; + experiment.activatedExperimentPayloads = [@[ payloadData1 ] mutableCopy]; + experiment.experimentPayloads = [@[] mutableCopy]; + + NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; + XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); +} + +- (void)testExperimentDiff_noChange { + FIRExperimentController *experimentController = + [[FIRExperimentController alloc] initWithAnalytics:nil]; + id mockExperimentController = OCMPartialMock(experimentController); + RCNConfigExperiment *experiment = + [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock + experimentController:mockExperimentController]; + + NSData *payloadData1 = [[self class] payloadDataFromTestFile]; + experiment.activatedExperimentPayloads = [@[ payloadData1 ] mutableCopy]; + experiment.experimentPayloads = [@[ payloadData1 ] mutableCopy]; + + NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; + XCTAssertTrue([changedKeys count] == 0); +} + #pragma mark Helpers. - (ABTExperimentPayload *)deserializeABTData:(NSData *)payload { diff --git a/FirebaseRemoteConfig/Tests/Unit/TestABTPayload.txt b/FirebaseRemoteConfig/Tests/Unit/TestABTPayload.txt index fb0e71cc54f..ef3ca9f76e3 100644 --- a/FirebaseRemoteConfig/Tests/Unit/TestABTPayload.txt +++ b/FirebaseRemoteConfig/Tests/Unit/TestABTPayload.txt @@ -15,5 +15,6 @@ { "experimentId": "exp_1" } - ] + ], + "affectedParameterKeys": ["test_key_1"] } From bac29da3bda8517ea2a2e05058e5544d42e39c2b Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Thu, 18 May 2023 15:08:55 -0700 Subject: [PATCH 16/28] Add in commenting --- .../Sources/RCNConfigExperiment.m | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 63d709c3960..9abcf8dc533 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -212,6 +212,7 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist options:NSJSONReadingMutableContainers error:&error]; if (!error && experimentJSON) { + /// Map experiments to experiment ID. [experimentsMap setObject:experimentJSON forKey:[experimentJSON valueForKey:kExperimentIdKey]]; } @@ -228,11 +229,13 @@ - (NSMutableArray *)extractConfigKeysFromExperiment:(NSDictionary *)experiment { return (NSMutableArray *)[experiment objectForKey:kAffectedParameterKeys]; } -- (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment - fetchedExperiment:(NSDictionary *)fetchedExperiment { +- (bool)isExperimentMetadataUnchanged:(NSDictionary *)activeExperiment + fetchedExperiment:(NSDictionary *)fetchedExperiment { + /// Create copies of active and fetched experiments. NSMutableDictionary *activeExperimentCopy = [activeExperiment mutableCopy]; NSMutableDictionary *fetchedExperimentCopy = [fetchedExperiment mutableCopy]; + /// Remove config parameter keys from object since they don't show up in consistent order. if ([activeExperimentCopy objectForKey:kAffectedParameterKeys]) { [activeExperimentCopy removeObjectForKey:kAffectedParameterKeys]; } @@ -246,18 +249,22 @@ - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment - (NSMutableSet *)getChangedExperimentConfigKeys:(NSMutableArray *)activeExperimentKeys fetchedExperimentKeys: (NSMutableArray *)fetchedExperimentKeys { - NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + NSMutableSet *allKeys = [[NSMutableSet alloc] init]; NSMutableSet *activeKeys = [[NSMutableSet alloc] init]; NSMutableSet *fetchedKeys = [[NSMutableSet alloc] init]; + /// Init keys set with experiment keys. [activeKeys addObjectsFromArray:activeExperimentKeys]; [fetchedKeys addObjectsFromArray:fetchedExperimentKeys]; - changedKeys = [[changedKeys setByAddingObjectsFromSet:activeKeys] mutableCopy]; - changedKeys = [[changedKeys setByAddingObjectsFromSet:fetchedKeys] mutableCopy]; + /// Add all keys into a single set. + allKeys = [[allKeys setByAddingObjectsFromSet:activeKeys] mutableCopy]; + allKeys = [[allKeys setByAddingObjectsFromSet:fetchedKeys] mutableCopy]; - NSSet *allKeys = [changedKeys copy]; + NSMutableSet *changedKeys = [allKeys mutableCopy]; + /// Iterate through all possible keys. for (NSString *key in allKeys) { + /// If keys are present in both active and fetched sets, remove from `changedKeys`. if ([activeKeys containsObject:key] && [fetchedKeys containsObject:key]) { [changedKeys removeObject:key]; } @@ -278,9 +285,13 @@ - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment [allExperimentIds addObjectsFromArray:[fetchedExperiments allKeys]]; [allExperimentIds addObjectsFromArray:[activeExperiments allKeys]]; + /// Iterate through all possible experiment IDs. for (NSString *experimentId in allExperimentIds) { + /// If an experiment ID doesn't exist one of the maps then an experiment must have been + /// added/removed. Add it's keys into `changedKeys`. if (![activeExperiments objectForKey:experimentId] || ![fetchedExperiments objectForKey:experimentId]) { + /// Get the experiment that was altered. NSDictionary *experiment; if ([activeExperiments objectForKey:experimentId]) { experiment = [activeExperiments objectForKey:experimentId]; @@ -288,20 +299,27 @@ - (bool)isExperimentMetadataSame:(NSDictionary *)activeExperiment experiment = [fetchedExperiments objectForKey:experimentId]; } + /// Add all of it's keys into `changedKeys`. [changedKeys addObjectsFromArray:[self extractConfigKeysFromExperiment:experiment]]; } else { + /// Fetched and Active contain the experiment ID. The metadata needs to be compared to see if + /// they're still the same. NSDictionary *activeExperiment = [activeExperiments objectForKey:experimentId]; NSDictionary *fetchedExperiment = [fetchedExperiments objectForKey:experimentId]; + /// Extract keys from active and fetched experiments. NSMutableArray *activeExperimentKeys = [self extractConfigKeysFromExperiment:activeExperiment]; NSMutableArray *fetchedExperimentKeys = [self extractConfigKeysFromExperiment:fetchedExperiment]; - if (![self isExperimentMetadataSame:activeExperiment fetchedExperiment:fetchedExperiment]) { + if (![self isExperimentMetadataUnchanged:activeExperiment + fetchedExperiment:fetchedExperiment]) { + /// Add in all keys from both sides if the experiments metadata has changed. [changedKeys addObjectsFromArray:activeExperimentKeys]; [changedKeys addObjectsFromArray:fetchedExperimentKeys]; } else { + /// Compare config keys from either experiment. changedKeys = [[changedKeys setByAddingObjectsFromSet:[self getChangedExperimentConfigKeys:activeExperimentKeys fetchedExperimentKeys:fetchedExperimentKeys]] From 829d953ce893a3b9959157397d48d4b310a3d70a Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Wed, 24 May 2023 11:56:12 -0700 Subject: [PATCH 17/28] Address PR comments --- FirebaseRemoteConfig/Sources/RCNConfigDBManager.m | 10 ++++++---- .../Sources/RCNConfigExperiment.m | 15 ++++++++------- .../Tests/Unit/RCNConfigExperimentTest.m | 8 ++++---- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index dd7b86daf39..6550760c16b 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -828,10 +828,10 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { } /// Load activated experiments payload. - NSMutableArray *activatedExperimentPayloads = + NSMutableArray *activeExperimentPayloads = [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyActivePayload]; - if (!activatedExperimentPayloads) { - activatedExperimentPayloads = [[NSMutableArray alloc] init]; + if (!activeExperimentPayloads) { + activeExperimentPayloads = [[NSMutableArray alloc] init]; } if (handler) { @@ -840,7 +840,9 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { YES, @{ @RCNExperimentTableKeyPayload : [experimentPayloads copy], @RCNExperimentTableKeyMetadata : [experimentMetadata copy], - @RCNExperimentTableKeyActivePayload : [activatedExperimentPayloads copy] + /// Activated experiments only need ExperimentsDescriptions data, which + /// experimentPayloads contains. + @RCNExperimentTableKeyActivePayload : [activeExperimentPayloads copy] }); }); } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index e587b451832..f5d09ea43a6 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -33,7 +33,7 @@ @interface RCNConfigExperiment () @property(nonatomic, strong) NSMutableDictionary *experimentMetadata; ///< Experiment metadata @property(nonatomic, strong) - NSMutableArray *activatedExperimentPayloads; ///< Activated experiment payloads. + NSMutableArray *activeExperimentPayloads; ///< Activated experiment payloads. @property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. @property(nonatomic, strong) FIRExperimentController *experimentController; @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter; @@ -47,6 +47,7 @@ - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager if (self) { _experimentPayloads = [[NSMutableArray alloc] init]; _experimentMetadata = [[NSMutableDictionary alloc] init]; + _activeExperimentPayloads = [[NSMutableArray alloc] init]; _experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init]; [_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; @@ -94,7 +95,7 @@ - (void)loadExperimentFromTable { /// Load activated experiments payload and metadata. if (result[@RCNExperimentTableKeyActivePayload]) { - [strongSelf->_activatedExperimentPayloads removeAllObjects]; + [strongSelf->_activeExperimentPayloads removeAllObjects]; for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) { NSError *error; id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment @@ -104,7 +105,7 @@ - (void)loadExperimentFromTable { FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031", @"Activated experiment payload could not be parsed as JSON."); } else { - [strongSelf->_activatedExperimentPayloads addObject:experiment]; + [strongSelf->_activeExperimentPayloads addObject:experiment]; } } } @@ -152,7 +153,7 @@ - (void)updateExperimentsWithHandler:(void (^)(NSError *_Nullable))handler { completionHandler:handler]; /// Update activated experiments payload and metadata in DB. - [self updateActiveExperiments]; + [self updateActiveExperimentsInDB]; } - (void)updateExperimentStartTime { @@ -179,12 +180,12 @@ - (void)updateExperimentStartTime { completionHandler:nil]; } -- (void)updateActiveExperiments { +- (void)updateActiveExperimentsInDB { /// Put current fetched experiment payloads into activated experiment DB. - [_activatedExperimentPayloads removeAllObjects]; + [_activeExperimentPayloads removeAllObjects]; [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload]; for (NSData *experiment in _experimentPayloads) { - [_activatedExperimentPayloads addObject:experiment]; + [_activeExperimentPayloads addObject:experiment]; [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload value:experiment completionHandler:nil]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m index 268f8758a7d..759b6317bfe 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -38,11 +38,11 @@ - (instancetype)initWithAnalytics:(nullable id)analytics; @interface RCNConfigExperiment () @property(nonatomic, copy) NSMutableArray *experimentPayloads; @property(nonatomic, copy) NSMutableDictionary *experimentMetadata; -@property(nonatomic, copy) NSMutableArray *activatedExperimentPayloads; +@property(nonatomic, copy) NSMutableArray *activeExperimentPayloads; @property(nonatomic, strong) RCNConfigDBManager *DBManager; - (NSTimeInterval)updateExperimentStartTime; - (void)loadExperimentFromTable; -- (void)updateActiveExperiments; +- (void)updateActiveExperimentsInDB; @end @interface RCNConfigExperimentTest : XCTestCase { @@ -234,8 +234,8 @@ - (void)testUpdateExperiments { XCTAssertNil(error); XCTAssertEqualObjects(experiment.experimentMetadata[@"last_experiment_start_time"], @(12345678)); - OCMVerify([experiment updateActiveExperiments]); - XCTAssertEqualObjects(experiment.activatedExperimentPayloads, @[ payloadData ]); + OCMVerify([experiment updateActiveExperimentsInDB]); + XCTAssertEqualObjects(experiment.activeExperimentPayloads, @[ payloadData ]); }]; } From 9a0f4e6e4956b1731f00172b934b4953075654b9 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Fri, 26 May 2023 10:03:51 -0700 Subject: [PATCH 18/28] update naming --- FirebaseRemoteConfig/Sources/RCNConfigDBManager.m | 8 ++++---- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 12 ++++++------ .../Tests/Unit/RCNConfigExperimentTest.m | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index dd7b86daf39..d1abdd1abf9 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -828,10 +828,10 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { } /// Load activated experiments payload. - NSMutableArray *activatedExperimentPayloads = + NSMutableArray *activeExperimentPayloads = [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyActivePayload]; - if (!activatedExperimentPayloads) { - activatedExperimentPayloads = [[NSMutableArray alloc] init]; + if (!activeExperimentPayloads) { + activeExperimentPayloads = [[NSMutableArray alloc] init]; } if (handler) { @@ -840,7 +840,7 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { YES, @{ @RCNExperimentTableKeyPayload : [experimentPayloads copy], @RCNExperimentTableKeyMetadata : [experimentMetadata copy], - @RCNExperimentTableKeyActivePayload : [activatedExperimentPayloads copy] + @RCNExperimentTableKeyActivePayload : [activeExperimentPayloads copy] }); }); } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 9abcf8dc533..836085d91c9 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -36,7 +36,7 @@ @interface RCNConfigExperiment () @property(nonatomic, strong) NSMutableDictionary *experimentMetadata; ///< Experiment metadata @property(nonatomic, strong) - NSMutableArray *activatedExperimentPayloads; ///< Activated experiment payloads. + NSMutableArray *activeExperimentPayloads; ///< Activated experiment payloads. @property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. @property(nonatomic, strong) FIRExperimentController *experimentController; @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter; @@ -97,7 +97,7 @@ - (void)loadExperimentFromTable { /// Load activated experiments payload and metadata. if (result[@RCNExperimentTableKeyActivePayload]) { - [strongSelf->_activatedExperimentPayloads removeAllObjects]; + [strongSelf->_activeExperimentPayloads removeAllObjects]; for (NSData *experiment in result[@RCNExperimentTableKeyActivePayload]) { NSError *error; id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment @@ -107,7 +107,7 @@ - (void)loadExperimentFromTable { FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031", @"Activated experiment payload could not be parsed as JSON."); } else { - [strongSelf->_activatedExperimentPayloads addObject:experiment]; + [strongSelf->_activeExperimentPayloads addObject:experiment]; } } } @@ -184,10 +184,10 @@ - (void)updateExperimentStartTime { - (void)updateActiveExperiments { /// Put current fetched experiment payloads into activated experiment DB. - [_activatedExperimentPayloads removeAllObjects]; + [_activeExperimentPayloads removeAllObjects]; [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyActivePayload]; for (NSData *experiment in _experimentPayloads) { - [_activatedExperimentPayloads addObject:experiment]; + [_activeExperimentPayloads addObject:experiment]; [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyActivePayload value:experiment completionHandler:nil]; @@ -277,7 +277,7 @@ - (bool)isExperimentMetadataUnchanged:(NSDictionary *)activeExperiment NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; NSMutableDictionary *activeExperiments = - [self getExperimentsMap:_activatedExperimentPayloads]; + [self getExperimentsMap:_activeExperimentPayloads]; NSMutableDictionary *fetchedExperiments = [self getExperimentsMap:_experimentPayloads]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m index 1bd15cb4328..27be9d45984 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -38,7 +38,7 @@ - (instancetype)initWithAnalytics:(nullable id)analytics; @interface RCNConfigExperiment () @property(nonatomic, copy) NSMutableArray *experimentPayloads; @property(nonatomic, copy) NSMutableDictionary *experimentMetadata; -@property(nonatomic, copy) NSMutableArray *activatedExperimentPayloads; +@property(nonatomic, copy) NSMutableArray *activeExperimentPayloads; @property(nonatomic, strong) RCNConfigDBManager *DBManager; - (NSTimeInterval)updateExperimentStartTime; - (void)loadExperimentFromTable; @@ -236,7 +236,7 @@ - (void)testUpdateExperiments { XCTAssertEqualObjects(experiment.experimentMetadata[@"last_experiment_start_time"], @(12345678)); OCMVerify([experiment updateActiveExperiments]); - XCTAssertEqualObjects(experiment.activatedExperimentPayloads, @[ payloadData ]); + XCTAssertEqualObjects(experiment.activeExperimentPayloads, @[ payloadData ]); }]; } @@ -249,7 +249,7 @@ - (void)testExperimentDiff_addedExperiment { experimentController:mockExperimentController]; NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.activatedExperimentPayloads = [@[ payloadData1 ] mutableCopy]; + experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy]; NSError *dataError; NSMutableDictionary *payload = @@ -276,7 +276,7 @@ - (void)testExperimentDiff_changedExperimentMetadata { experimentController:mockExperimentController]; NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.activatedExperimentPayloads = [@[ payloadData1 ] mutableCopy]; + experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy]; NSError *dataError; NSMutableDictionary *payload = @@ -303,7 +303,7 @@ - (void)testExperimentDiff_changedExperimentKeys { experimentController:mockExperimentController]; NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.activatedExperimentPayloads = [@[ payloadData1 ] mutableCopy]; + experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy]; NSError *dataError; NSMutableDictionary *payload = @@ -330,7 +330,7 @@ - (void)testExperimentDiff_deletedExperiment { experimentController:mockExperimentController]; NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.activatedExperimentPayloads = [@[ payloadData1 ] mutableCopy]; + experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy]; experiment.experimentPayloads = [@[] mutableCopy]; NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; @@ -346,7 +346,7 @@ - (void)testExperimentDiff_noChange { experimentController:mockExperimentController]; NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.activatedExperimentPayloads = [@[ payloadData1 ] mutableCopy]; + experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy]; experiment.experimentPayloads = [@[ payloadData1 ] mutableCopy]; NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; From 0ce47b9c644c1697680bfc7593c8a9580f9df59d Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Fri, 26 May 2023 10:44:16 -0700 Subject: [PATCH 19/28] formatting --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 6fe73d06747..30938e8b4db 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -36,7 +36,7 @@ @interface RCNConfigExperiment () @property(nonatomic, strong) NSMutableDictionary *experimentMetadata; ///< Experiment metadata @property(nonatomic, strong) - NSMutableArray *activeExperimentPayloads; ///< Activated experiment payloads. + NSMutableArray *activeExperimentPayloads; ///< Activated experiment payloads. @property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. @property(nonatomic, strong) FIRExperimentController *experimentController; @property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter; From 5166676fbfb69681e6ac871832eae7b6af865957 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Mon, 5 Jun 2023 16:02:47 -0700 Subject: [PATCH 20/28] Update diffing logic --- .../Sources/RCNConfigExperiment.m | 137 +++++------------- 1 file changed, 34 insertions(+), 103 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 30938e8b4db..c2c078f1e92 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -201,11 +201,11 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist andPayloads:_experimentPayloads]; } -- (NSMutableDictionary *)getExperimentsMap: - (NSMutableArray *)experiments { - NSMutableDictionary *experimentsMap = +- (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experiments { + 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 = @@ -213,118 +213,49 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist options:NSJSONReadingMutableContainers error:&error]; if (!error && experimentJSON) { - /// Map experiments to experiment ID. - [experimentsMap setObject:experimentJSON - forKey:[experimentJSON valueForKey:kExperimentIdKey]]; + if ([experimentJSON objectForKey:kAffectedParameterKeys]) { + NSMutableArray *configKeys = + (NSMutableArray *)[experimentJSON objectForKey:kAffectedParameterKeys]; + NSMutableDictionary *experimentCopy = [experimentJSON mutableCopy]; + /// Remote `affectedParameterKeys` because it comes 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; } -- (NSMutableArray *)extractConfigKeysFromExperiment:(NSDictionary *)experiment { - if (![experiment objectForKey:kAffectedParameterKeys]) { - return [[NSMutableArray alloc] init]; - } - - return (NSMutableArray *)[experiment objectForKey:kAffectedParameterKeys]; -} - -- (bool)isExperimentMetadataUnchanged:(NSDictionary *)activeExperiment - fetchedExperiment:(NSDictionary *)fetchedExperiment { - /// Create copies of active and fetched experiments. - NSMutableDictionary *activeExperimentCopy = [activeExperiment mutableCopy]; - NSMutableDictionary *fetchedExperimentCopy = [fetchedExperiment mutableCopy]; - - /// Remove config parameter keys from object since they don't show up in consistent order. - if ([activeExperimentCopy objectForKey:kAffectedParameterKeys]) { - [activeExperimentCopy removeObjectForKey:kAffectedParameterKeys]; - } - if ([fetchedExperimentCopy objectForKey:kAffectedParameterKeys]) { - [fetchedExperimentCopy removeObjectForKey:kAffectedParameterKeys]; - } - - return [activeExperimentCopy isEqualToDictionary:fetchedExperimentCopy]; -} - -- (NSMutableSet *)getChangedExperimentConfigKeys:(NSMutableArray *)activeExperimentKeys - fetchedExperimentKeys: - (NSMutableArray *)fetchedExperimentKeys { - NSMutableSet *allKeys = [[NSMutableSet alloc] init]; - NSMutableSet *activeKeys = [[NSMutableSet alloc] init]; - NSMutableSet *fetchedKeys = [[NSMutableSet alloc] init]; - - /// Init keys set with experiment keys. - [activeKeys addObjectsFromArray:activeExperimentKeys]; - [fetchedKeys addObjectsFromArray:fetchedExperimentKeys]; - /// Add all keys into a single set. - allKeys = [[allKeys setByAddingObjectsFromSet:activeKeys] mutableCopy]; - allKeys = [[allKeys setByAddingObjectsFromSet:fetchedKeys] mutableCopy]; - - NSMutableSet *changedKeys = [allKeys mutableCopy]; - - /// Iterate through all possible keys. - for (NSString *key in allKeys) { - /// If keys are present in both active and fetched sets, remove from `changedKeys`. - if ([activeKeys containsObject:key] && [fetchedKeys containsObject:key]) { - [changedKeys removeObject:key]; - } - } - - return changedKeys; -} - - (NSMutableSet *)getKeysAffectedByChangedExperiments { NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + NSMutableDictionary *activeExperimentsMap = [self createExperimentsMap:_activeExperimentPayloads]; + NSMutableDictionary *fetchedExperimentsMap = [self createExperimentsMap:_experimentPayloads]; - NSMutableDictionary *activeExperiments = - [self getExperimentsMap:_activeExperimentPayloads]; - NSMutableDictionary *fetchedExperiments = - [self getExperimentsMap:_experimentPayloads]; - - NSMutableSet *allExperimentIds = [[NSMutableSet alloc] init]; - [allExperimentIds addObjectsFromArray:[fetchedExperiments allKeys]]; - [allExperimentIds addObjectsFromArray:[activeExperiments allKeys]]; - - /// Iterate through all possible experiment IDs. - for (NSString *experimentId in allExperimentIds) { - /// If an experiment ID doesn't exist one of the maps then an experiment must have been - /// added/removed. Add it's keys into `changedKeys`. - if (![activeExperiments objectForKey:experimentId] || - ![fetchedExperiments objectForKey:experimentId]) { - /// Get the experiment that was altered. - NSDictionary *experiment; - if ([activeExperiments objectForKey:experimentId]) { - experiment = [activeExperiments objectForKey:experimentId]; - } else { - experiment = [fetchedExperiments objectForKey:experimentId]; + 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]; } + } + } - /// Add all of it's keys into `changedKeys`. - [changedKeys addObjectsFromArray:[self extractConfigKeysFromExperiment:experiment]]; + /// + for (NSString *key in [fetchedExperimentsMap allKeys]) { + if (![activeExperimentsMap objectForKey:key]) { + [changedKeys addObject:key]; } else { - /// Fetched and Active contain the experiment ID. The metadata needs to be compared to see if - /// they're still the same. - NSDictionary *activeExperiment = [activeExperiments objectForKey:experimentId]; - NSDictionary *fetchedExperiment = [fetchedExperiments objectForKey:experimentId]; - - /// Extract keys from active and fetched experiments. - NSMutableArray *activeExperimentKeys = - [self extractConfigKeysFromExperiment:activeExperiment]; - NSMutableArray *fetchedExperimentKeys = - [self extractConfigKeysFromExperiment:fetchedExperiment]; - - if (![self isExperimentMetadataUnchanged:activeExperiment - fetchedExperiment:fetchedExperiment]) { - /// Add in all keys from both sides if the experiments metadata has changed. - [changedKeys addObjectsFromArray:activeExperimentKeys]; - [changedKeys addObjectsFromArray:fetchedExperimentKeys]; - } else { - /// Compare config keys from either experiment. - changedKeys = [[changedKeys - setByAddingObjectsFromSet:[self getChangedExperimentConfigKeys:activeExperimentKeys - fetchedExperimentKeys:fetchedExperimentKeys]] - mutableCopy]; + if (![[fetchedExperimentsMap objectForKey:key] + isEqualToDictionary:[activeExperimentsMap objectForKey:key]]) { + [changedKeys addObject:key]; } } } From 756df3b38cf40898590391dcd273321e18f09d5e Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Mon, 5 Jun 2023 16:31:03 -0700 Subject: [PATCH 21/28] format and add comments --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index c2c078f1e92..00d27ec752f 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -201,6 +201,7 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist andPayloads:_experimentPayloads]; } +/// Creates a map where the key is the config key and the value if the experiment description. - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experiments { NSMutableDictionary *experimentsMap = [[NSMutableDictionary alloc] init]; @@ -232,11 +233,14 @@ - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experi return experimentsMap; } +/// Returns keys that were affected by experiment changes. - (NSMutableSet *)getKeysAffectedByChangedExperiments { NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + /// Create config keys to experiments map. NSMutableDictionary *activeExperimentsMap = [self createExperimentsMap:_activeExperimentPayloads]; NSMutableDictionary *fetchedExperimentsMap = [self createExperimentsMap:_experimentPayloads]; + /// Iterate through active experiement's keys and compare them to fetched experiment's keys. for (NSString *key in [activeExperimentsMap allKeys]) { if (![fetchedExperimentsMap objectForKey:key]) { [changedKeys addObject:key]; @@ -248,7 +252,7 @@ - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experi } } - /// + /// Iterate through active experiement's keys and compare them to fetched experiment's keys. for (NSString *key in [fetchedExperimentsMap allKeys]) { if (![activeExperimentsMap objectForKey:key]) { [changedKeys addObject:key]; From a1a01e635dd4f068307801fc87f229d1e80a1759 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Mon, 5 Jun 2023 16:32:27 -0700 Subject: [PATCH 22/28] Fix grammar --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 00d27ec752f..463f2b191e9 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -240,7 +240,7 @@ - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experi NSMutableDictionary *activeExperimentsMap = [self createExperimentsMap:_activeExperimentPayloads]; NSMutableDictionary *fetchedExperimentsMap = [self createExperimentsMap:_experimentPayloads]; - /// Iterate through active experiement's keys and compare them to fetched experiment's keys. + /// 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]; @@ -252,7 +252,7 @@ - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experi } } - /// Iterate through active experiement's keys and compare them to fetched experiment's keys. + /// 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]; From 00d8284c8919738da2c3f5bdb3a3af1e1f6316c1 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Mon, 5 Jun 2023 16:33:35 -0700 Subject: [PATCH 23/28] Update comment --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 463f2b191e9..aec8984ddc5 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -218,7 +218,7 @@ - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experi NSMutableArray *configKeys = (NSMutableArray *)[experimentJSON objectForKey:kAffectedParameterKeys]; NSMutableDictionary *experimentCopy = [experimentJSON mutableCopy]; - /// Remote `affectedParameterKeys` because it comes out of order and could affect the + /// Remove `affectedParameterKeys` because the values come out of order and could affect the /// diffing. [experimentCopy removeObjectForKey:kAffectedParameterKeys]; From 693a2debdd9bfacd08c48a2d28de7b93322a7cc9 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Mon, 5 Jun 2023 16:34:46 -0700 Subject: [PATCH 24/28] Remove unused constant --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.m | 1 - 1 file changed, 1 deletion(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index aec8984ddc5..677891e1d27 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -27,7 +27,6 @@ static NSString *const kMethodNameLatestStartTime = @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:"; -static NSString *const kExperimentIdKey = @"experimentId"; static NSString *const kAffectedParameterKeys = @"affectedParameterKeys"; @interface RCNConfigExperiment () From 9275933a19e750c3a1677ba80e088a5e3a1ef49b Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Tue, 6 Jun 2023 09:30:59 -0700 Subject: [PATCH 25/28] Move diffing logic to ConfigContainer --- .../Sources/RCNConfigContent.h | 4 +- .../Sources/RCNConfigContent.m | 104 +++++++++- .../Sources/RCNConfigExperiment.h | 2 - .../Sources/RCNConfigExperiment.m | 68 ------- FirebaseRemoteConfig/Sources/RCNConfigFetch.m | 15 +- .../Tests/Unit/RCNConfigContentTest.m | 178 +++++++++++++++--- .../Tests/Unit/RCNConfigExperimentTest.m | 114 ----------- 7 files changed, 259 insertions(+), 226 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index 1c2a141cbc9..34d0895243a 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -66,8 +66,6 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; /// Returns the updated parameters between fetched and active config. -- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace - withExperimentChanges: - (NSMutableSet *)changedExperimentKeys; +- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace; @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 882c10f967a..b141c95e2ed 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -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; @@ -398,13 +400,107 @@ - (BOOL)checkAndWaitForInitialDatabaseLoad { return true; } +/// Load active and fetched experiment payloads and return them in a map. +- (NSDictionary *> *)loadExperimentsPayloads { + __block NSMutableArray *activeExperimentPayloads = [[NSMutableArray alloc] init]; + __block NSMutableArray *experimentPayloads = [[NSMutableArray alloc] init]; + + /// Load experiments from DB. + RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary *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 *)experiments { + 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 *)getKeysAffectedByChangedExperiments { + NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; + + /// Load experiments from DB. + NSDictionary *allExperimentPayloads = [self loadExperimentsPayloads]; + NSMutableArray *activeExperimentPayloads = + [allExperimentPayloads objectForKey:@RCNExperimentTableKeyActivePayload]; + NSMutableArray *experimentPayloads = + [allExperimentPayloads objectForKey:@RCNExperimentTableKeyPayload]; + + /// 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 - withExperimentChanges: - (NSMutableSet *)changedExperimentKeys { +- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace { FIRRemoteConfigUpdate *configUpdate; NSMutableSet *updatedKeys = [[NSMutableSet alloc] init]; - updatedKeys = [[updatedKeys setByAddingObjectsFromSet:changedExperimentKeys] mutableCopy]; + updatedKeys = [[self getKeysAffectedByChangedExperiments] mutableCopy]; NSDictionary *fetchedConfig = _fetchedConfig[FIRNamespace] ? _fetchedConfig[FIRNamespace] : [[NSDictionary alloc] init]; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h index de74fd196ed..57ef977b0d0 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h @@ -36,6 +36,4 @@ /// Update experiments to Firebase Analytics when `activateWithCompletion:` happens. - (void)updateExperimentsWithHandler:(nullable void (^)(NSError *_Nullable error))handler; -/// Return config keys from experiments that have changed. -- (NSMutableSet *_Nonnull)getKeysAffectedByChangedExperiments; @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m index 677891e1d27..f5d09ea43a6 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -27,8 +27,6 @@ static NSString *const kMethodNameLatestStartTime = @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:"; -static NSString *const kAffectedParameterKeys = @"affectedParameterKeys"; - @interface RCNConfigExperiment () @property(nonatomic, strong) NSMutableArray *experimentPayloads; ///< Experiment payloads. @@ -199,70 +197,4 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime andPayloads:_experimentPayloads]; } - -/// Creates a map where the key is the config key and the value if the experiment description. -- (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experiments { - 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 *)getKeysAffectedByChangedExperiments { - NSMutableSet *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; -} @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index 95977fbdb9f..c350ed3e086 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -216,9 +216,8 @@ - (void)realtimeFetchConfigWithNoExpirationDuration:(NSInteger)fetchAttemptNumbe if (strongSelf->_settings.lastFetchTimeInterval > 0) { FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000052", @"A fetch is already in progress. Using previous fetch results."); - FIRRemoteConfigUpdate *update = [self->_content - getConfigUpdateForNamespace:self->_FIRNamespace - withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]]; + FIRRemoteConfigUpdate *update = + [self->_content getConfigUpdateForNamespace:self->_FIRNamespace]; return [strongSelf reportCompletionWithStatus:strongSelf->_settings.lastFetchStatus withUpdate:update withError:nil @@ -538,9 +537,8 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties if (!data) { FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000043", @"RCN Fetch: No data in fetch response"); // There may still be a difference between fetched and active config - FIRRemoteConfigUpdate *update = [strongSelf->_content - getConfigUpdateForNamespace:strongSelf->_FIRNamespace - withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]]; + FIRRemoteConfigUpdate *update = + [strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace]; return [strongSelf reportCompletionWithStatus:FIRRemoteConfigFetchStatusSuccess withUpdate:update withError:nil @@ -617,9 +615,8 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties strongSelf->_settings.lastETag = latestETag; } // Compute config update after successful fetch - FIRRemoteConfigUpdate *update = [strongSelf->_content - getConfigUpdateForNamespace:strongSelf->_FIRNamespace - withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]]; + FIRRemoteConfigUpdate *update = + [strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace]; [strongSelf->_settings updateMetadataWithFetchSuccessStatus:YES diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index 37fc97fe073..24a6eb6de75 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -26,6 +26,8 @@ #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" @interface RCNConfigContent (Testing) +- (NSDictionary *> *)loadExperimentsPayloads; +- (NSMutableSet *)getKeysAffectedByChangedExperiments; - (BOOL)checkAndWaitForInitialDatabaseLoad; @end @@ -344,39 +346,148 @@ - (void)testConfigUpdate_noChange_emptyResponse { toSource:RCNDBSourceActive forNamespace:namespace]; - FIRRemoteConfigUpdate *update = - [_configContent getConfigUpdateForNamespace:namespace - withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; XCTAssertTrue([update updatedKeys].count == 0); } - (void)testConfigUpdate_noParamChange_butExperimentChange { NSString *namespace = @"test_namespace"; + RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; + NSMutableSet *experimentKeys = [[NSMutableSet alloc] init]; + [experimentKeys addObject:@"key_2"]; + id configMock = OCMPartialMock(configContent); + OCMStub([configMock getKeysAffectedByChangedExperiments]).andReturn(experimentKeys); // populate fetched config NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil]; - [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + [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}}; - [_configContent copyFromDictionary:namespaceToConfig - toSource:RCNDBSourceActive - forNamespace:namespace]; + [configMock copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; - NSMutableSet *experimentKeys = [[NSMutableSet alloc] init]; - [experimentKeys addObject:@"key_2"]; - FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace - withExperimentChanges:experimentKeys]; + NSMutableSet *experimentKey = [[NSMutableSet alloc] init]; + 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]; + id configMock = OCMPartialMock(configContent); + OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ + @"experiment_payload" : experimentPayloads, + @"experiment_active_payload" : activeExperimentPayloads + })); + NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + 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]; + id configMock = OCMPartialMock(configContent); + OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ + @"experiment_payload" : experimentPayloads, + @"experiment_active_payload" : activeExperimentPayloads + })); + NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + 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]; + id configMock = OCMPartialMock(configContent); + OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ + @"experiment_payload" : experimentPayloads, + @"experiment_active_payload" : activeExperimentPayloads + })); + NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + 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]; + id configMock = OCMPartialMock(configContent); + OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ + @"experiment_payload" : experimentPayloads, + @"experiment_active_payload" : activeExperimentPayloads + })); + NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + 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]; + id configMock = OCMPartialMock(configContent); + OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ + @"experiment_payload" : experimentPayloads, + @"experiment_active_payload" : activeExperimentPayloads + })); + NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + XCTAssertTrue([changedKeys count] == 0); +} + - (void)testConfigUpdate_paramAdded_returnsNewKey { NSString *namespace = @"test_namespace"; NSString *newParam = @"key2"; @@ -396,9 +507,7 @@ - (void)testConfigUpdate_paramAdded_returnsNewKey { p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = - [_configContent getConfigUpdateForNamespace:namespace - withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:newParam]); @@ -424,9 +533,7 @@ - (void)testConfigUpdate_paramValueChanged_returnsUpdatedKey { [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = - [_configContent getConfigUpdateForNamespace:namespace - withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); @@ -452,9 +559,7 @@ - (void)testConfigUpdate_paramDeleted_returnsDeletedKey { [self createFetchResponseWithConfigEntries:@{newParam : value1} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = - [_configContent getConfigUpdateForNamespace:namespace - withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; XCTAssertTrue([update updatedKeys].count == 2); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); // deleted @@ -489,9 +594,7 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { forKey:RCNFetchResponseKeyPersonalizationMetadata]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = - [_configContent getConfigUpdateForNamespace:namespace - withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); @@ -516,9 +619,7 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { [self createFetchResponseWithConfigEntries:@{existingParam : value1} p13nMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; - FIRRemoteConfigUpdate *update = - [_configContent getConfigUpdateForNamespace:namespace - withExperimentChanges:[[NSMutableSet alloc] init]]; + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; XCTAssertTrue([update updatedKeys].count == 1); XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); @@ -539,4 +640,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 diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m index f352183bf86..759b6317bfe 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -43,7 +43,6 @@ @interface RCNConfigExperiment () - (NSTimeInterval)updateExperimentStartTime; - (void)loadExperimentFromTable; - (void)updateActiveExperimentsInDB; -- (NSMutableSet *_Nonnull)getKeysAffectedByChangedExperiments; @end @interface RCNConfigExperimentTest : XCTestCase { @@ -240,119 +239,6 @@ - (void)testUpdateExperiments { }]; } -- (void)testExperimentDiff_addedExperiment { - FIRExperimentController *experimentController = - [[FIRExperimentController alloc] initWithAnalytics:nil]; - id mockExperimentController = OCMPartialMock(experimentController); - RCNConfigExperiment *experiment = - [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock - experimentController:mockExperimentController]; - - NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.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]; - experiment.experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy]; - - NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; - XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); -} - -- (void)testExperimentDiff_changedExperimentMetadata { - FIRExperimentController *experimentController = - [[FIRExperimentController alloc] initWithAnalytics:nil]; - id mockExperimentController = OCMPartialMock(experimentController); - RCNConfigExperiment *experiment = - [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock - experimentController:mockExperimentController]; - - NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.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]; - experiment.experimentPayloads = [@[ payloadData2 ] mutableCopy]; - - NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; - XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); -} - -- (void)testExperimentDiff_changedExperimentKeys { - FIRExperimentController *experimentController = - [[FIRExperimentController alloc] initWithAnalytics:nil]; - id mockExperimentController = OCMPartialMock(experimentController); - RCNConfigExperiment *experiment = - [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock - experimentController:mockExperimentController]; - - NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.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]; - experiment.experimentPayloads = [@[ payloadData2 ] mutableCopy]; - - NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; - XCTAssertTrue([changedKeys containsObject:@"test_key_2"]); -} - -- (void)testExperimentDiff_deletedExperiment { - FIRExperimentController *experimentController = - [[FIRExperimentController alloc] initWithAnalytics:nil]; - id mockExperimentController = OCMPartialMock(experimentController); - RCNConfigExperiment *experiment = - [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock - experimentController:mockExperimentController]; - - NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy]; - experiment.experimentPayloads = [@[] mutableCopy]; - - NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; - XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); -} - -- (void)testExperimentDiff_noChange { - FIRExperimentController *experimentController = - [[FIRExperimentController alloc] initWithAnalytics:nil]; - id mockExperimentController = OCMPartialMock(experimentController); - RCNConfigExperiment *experiment = - [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock - experimentController:mockExperimentController]; - - NSData *payloadData1 = [[self class] payloadDataFromTestFile]; - experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy]; - experiment.experimentPayloads = [@[ payloadData1 ] mutableCopy]; - - NSMutableSet *changedKeys = [experiment getKeysAffectedByChangedExperiments]; - XCTAssertTrue([changedKeys count] == 0); -} - #pragma mark Helpers. - (ABTExperimentPayload *)deserializeABTData:(NSData *)payload { From b68a3dfa78cb8565d8b63daf63c5ad9223f13de3 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Tue, 6 Jun 2023 09:33:56 -0700 Subject: [PATCH 26/28] remove space --- FirebaseRemoteConfig/Sources/RCNConfigExperiment.h | 1 - 1 file changed, 1 deletion(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h index 57ef977b0d0..79051faa87b 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h @@ -35,5 +35,4 @@ /// Update experiments to Firebase Analytics when `activateWithCompletion:` happens. - (void)updateExperimentsWithHandler:(nullable void (^)(NSError *_Nullable error))handler; - @end From 6dbc9c68abfc1900e622c92af1cb8318d5e8ee75 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Tue, 6 Jun 2023 09:34:23 -0700 Subject: [PATCH 27/28] Delete TestABTPayload2.txt --- TestABTPayload2.txt | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 TestABTPayload2.txt diff --git a/TestABTPayload2.txt b/TestABTPayload2.txt deleted file mode 100644 index 15da3ed607a..00000000000 --- a/TestABTPayload2.txt +++ /dev/null @@ -1,20 +0,0 @@ -{ - "experimentId": "exp_2", - "variantId": "var_1", - "triggerEvent": "customTrigger", - "experimentStartTime": "1970-05-23T21:21:18.000Z", - "triggerTimeoutMillis": 15552000000, - "timeToLiveMillis": 15552000000, - "setEventToLog": "set_event", - "activateEventToLog": "activate_event", - "clearEventToLog": "clear_event", - "timeoutEventToLog": "timeout_event", - "ttlExpiryEventToLog": "ttl_expiry_event", - "overflowPolicy": 2, - "ongoingExperiments": [ - { - "experimentId": "exp_1" - } - ], - "affectedParameterKey": ["test_key_1"] -} From 6e12a8ca14a7944ee62b502b81d93fcfacb6b6ae Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Tue, 6 Jun 2023 13:24:41 -0700 Subject: [PATCH 28/28] Make it easier to test --- .../Sources/RCNConfigContent.m | 23 +++++--- .../Tests/Unit/RCNConfigContentTest.m | 55 +++++++------------ 2 files changed, 35 insertions(+), 43 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index b141c95e2ed..8be21052356 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -455,16 +455,11 @@ - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experi } /// Returns keys that were affected by experiment changes. -- (NSMutableSet *)getKeysAffectedByChangedExperiments { +- (NSMutableSet *) + getKeysAffectedByChangedExperiments:(NSMutableArray *)activeExperimentPayloads + fetchedExperimentPayloads:(NSMutableArray *)experimentPayloads { NSMutableSet *changedKeys = [[NSMutableSet alloc] init]; - /// Load experiments from DB. - NSDictionary *allExperimentPayloads = [self loadExperimentsPayloads]; - NSMutableArray *activeExperimentPayloads = - [allExperimentPayloads objectForKey:@RCNExperimentTableKeyActivePayload]; - NSMutableArray *experimentPayloads = - [allExperimentPayloads objectForKey:@RCNExperimentTableKeyPayload]; - /// Create config keys to experiments map. NSMutableDictionary *activeExperimentsMap = [self createExperimentsMap:activeExperimentPayloads]; NSMutableDictionary *fetchedExperimentsMap = [self createExperimentsMap:experimentPayloads]; @@ -500,7 +495,12 @@ - (NSMutableDictionary *)createExperimentsMap:(NSMutableArray *)experi - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace { FIRRemoteConfigUpdate *configUpdate; NSMutableSet *updatedKeys = [[NSMutableSet alloc] init]; - updatedKeys = [[self getKeysAffectedByChangedExperiments] mutableCopy]; + + NSDictionary *experiments = [self loadExperimentsPayloads]; + NSMutableSet *changedExperimentKeys = [self + getKeysAffectedByChangedExperiments:[experiments + objectForKey:@RCNExperimentTableKeyActivePayload] + fetchedExperimentPayloads:[experiments objectForKey:@RCNExperimentTableKeyPayload]]; NSDictionary *fetchedConfig = _fetchedConfig[FIRNamespace] ? _fetchedConfig[FIRNamespace] : [[NSDictionary alloc] init]; @@ -536,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; } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index 24a6eb6de75..962611259a6 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -26,8 +26,9 @@ #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" @interface RCNConfigContent (Testing) -- (NSDictionary *> *)loadExperimentsPayloads; -- (NSMutableSet *)getKeysAffectedByChangedExperiments; +- (NSMutableSet *) + getKeysAffectedByChangedExperiments:(NSMutableArray *)activeExperimentPayloads + fetchedExperimentPayloads:(NSMutableArray *)experimentPayloads; - (BOOL)checkAndWaitForInitialDatabaseLoad; @end @@ -357,7 +358,9 @@ - (void)testConfigUpdate_noParamChange_butExperimentChange { NSMutableSet *experimentKeys = [[NSMutableSet alloc] init]; [experimentKeys addObject:@"key_2"]; id configMock = OCMPartialMock(configContent); - OCMStub([configMock getKeysAffectedByChangedExperiments]).andReturn(experimentKeys); + OCMStub([configMock getKeysAffectedByChangedExperiments:OCMOCK_ANY + fetchedExperimentPayloads:OCMOCK_ANY]) + .andReturn(experimentKeys); // populate fetched config NSMutableDictionary *fetchResponse = @@ -373,7 +376,6 @@ - (void)testConfigUpdate_noParamChange_butExperimentChange { toSource:RCNDBSourceActive forNamespace:namespace]; - NSMutableSet *experimentKey = [[NSMutableSet alloc] init]; FIRRemoteConfigUpdate *update = [configMock getConfigUpdateForNamespace:namespace]; XCTAssertTrue([update updatedKeys].count == 1); @@ -397,12 +399,9 @@ - (void)testExperimentDiff_addedExperiment { NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy]; RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; - id configMock = OCMPartialMock(configContent); - OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ - @"experiment_payload" : experimentPayloads, - @"experiment_active_payload" : activeExperimentPayloads - })); - NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + NSMutableSet *changedKeys = + [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads + fetchedExperimentPayloads:experimentPayloads]; XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); } @@ -423,12 +422,9 @@ - (void)testExperimentDiff_changedExperimentMetadata { NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy]; RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; - id configMock = OCMPartialMock(configContent); - OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ - @"experiment_payload" : experimentPayloads, - @"experiment_active_payload" : activeExperimentPayloads - })); - NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + NSMutableSet *changedKeys = + [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads + fetchedExperimentPayloads:experimentPayloads]; XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); } @@ -449,12 +445,9 @@ - (void)testExperimentDiff_changedExperimentKeys { NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy]; RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; - id configMock = OCMPartialMock(configContent); - OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ - @"experiment_payload" : experimentPayloads, - @"experiment_active_payload" : activeExperimentPayloads - })); - NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + NSMutableSet *changedKeys = + [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads + fetchedExperimentPayloads:experimentPayloads]; XCTAssertTrue([changedKeys containsObject:@"test_key_2"]); } @@ -464,12 +457,9 @@ - (void)testExperimentDiff_deletedExperiment { NSMutableArray *experimentPayloads = [@[] mutableCopy]; RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; - id configMock = OCMPartialMock(configContent); - OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ - @"experiment_payload" : experimentPayloads, - @"experiment_active_payload" : activeExperimentPayloads - })); - NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + NSMutableSet *changedKeys = + [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads + fetchedExperimentPayloads:experimentPayloads]; XCTAssertTrue([changedKeys containsObject:@"test_key_1"]); } @@ -479,12 +469,9 @@ - (void)testExperimentDiff_noChange { NSMutableArray *experimentPayloads = [@[ payloadData1 ] mutableCopy]; RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; - id configMock = OCMPartialMock(configContent); - OCMStub([configMock loadExperimentsPayloads]).andReturn((@{ - @"experiment_payload" : experimentPayloads, - @"experiment_active_payload" : activeExperimentPayloads - })); - NSMutableSet *changedKeys = [configMock getKeysAffectedByChangedExperiments]; + NSMutableSet *changedKeys = + [configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads + fetchedExperimentPayloads:experimentPayloads]; XCTAssertTrue([changedKeys count] == 0); }