From c2ca71c5e757ef9485529bbcbf7875aa166d7cfb Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 11 Jun 2026 18:25:10 +0600 Subject: [PATCH] Revert local holdout feature gate logic Remove the FeatureGates.localHoldouts flag and unwrap local holdout evaluation so it runs unconditionally, matching the pre-gate behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 16 -- .../DefaultDecisionService.swift | 52 ++-- Sources/Utils/Constants.swift | 4 - .../BaseHoldoutTests.swift | 27 --- .../DecisionListenerTest_Holdouts.swift | 11 +- .../DecisionServiceTests_Holdouts.swift | 7 +- .../DecisionServiceTests_LocalHoldouts.swift | 7 +- .../FeatureGateTests_LocalHoldouts.swift | 224 ------------------ ...zelyUserContextTests_Decide_Holdouts.swift | 9 +- ...xtTests_Decide_With_Holdouts_Reasons.swift | 10 +- 10 files changed, 38 insertions(+), 329 deletions(-) delete mode 100644 Tests/OptimizelyTests-Common/BaseHoldoutTests.swift delete mode 100644 Tests/OptimizelyTests-Common/FeatureGateTests_LocalHoldouts.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 27dbcf31..828d69b5 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -2121,12 +2121,6 @@ 98AC985F2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */; }; 98C2DF242F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */; }; 98C2DF252F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */; }; - 0921CDD1BC7F41F59B2D4CC3 /* FeatureGateTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5D873BB4C44181B07ECC15 /* FeatureGateTests_LocalHoldouts.swift */; }; - BB21E5C3E29D4F0C892E523C /* FeatureGateTests_LocalHoldouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5D873BB4C44181B07ECC15 /* FeatureGateTests_LocalHoldouts.swift */; }; - 98C2DF752FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; - 98C2DF762FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; - 98C2DF772FA8D1FF003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; - 98C2DF782FA8D207003F2443 /* BaseHoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */; }; 98D5AE842DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98D5AE832DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift */; }; 98D5AE852DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98D5AE832DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift */; }; 98F28A1D2E01940500A86546 /* Cmab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F28A1C2E01940500A86546 /* Cmab.swift */; }; @@ -2644,8 +2638,6 @@ 98AC98482DB8FC29001405DD /* DecisionServiceTests_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_Holdouts.swift; sourceTree = ""; }; 98AC985D2DBA6721001405DD /* OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift; sourceTree = ""; }; 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_LocalHoldouts.swift; sourceTree = ""; }; - 7C5D873BB4C44181B07ECC15 /* FeatureGateTests_LocalHoldouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureGateTests_LocalHoldouts.swift; sourceTree = ""; }; - 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseHoldoutTests.swift; sourceTree = ""; }; 98D5AE832DBB91C0000D5844 /* OptimizelyUserContextTests_Decide_Holdouts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_Holdouts.swift; sourceTree = ""; }; 98F28A1C2E01940500A86546 /* Cmab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cmab.swift; sourceTree = ""; }; 98F28A2D2E01968000A86546 /* CmabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmabTests.swift; sourceTree = ""; }; @@ -3177,7 +3169,6 @@ 6E75198022C5211100B2B157 /* DecisionServiceTests_Experiments.swift */, 6E75199122C5211100B2B157 /* DecisionServiceTests_Features.swift */, 98AC98482DB8FC29001405DD /* DecisionServiceTests_Holdouts.swift */, - 98C2DF742FA8D055003F2443 /* BaseHoldoutTests.swift */, 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */, 6E75198622C5211100B2B157 /* DecisionServiceTests_UserProfiles.swift */, 6E75198822C5211100B2B157 /* DefaultLoggerTests.swift */, @@ -3190,7 +3181,6 @@ 6E75198B22C5211100B2B157 /* NotificationCenterTests.swift */, 84861810286D0B8900B7F41B /* OdpEventManagerTests.swift */, 98C2DF232F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift */, - 7C5D873BB4C44181B07ECC15 /* FeatureGateTests_LocalHoldouts.swift */, 8486180E286D0B8900B7F41B /* OdpManagerTests.swift */, 8486180D286D0B8900B7F41B /* OdpSegmentManagerTests.swift */, 98F28A512E02E81500A86546 /* CMABClientTests.swift */, @@ -5134,7 +5124,6 @@ 6E75192D22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7516D322C520D400B2B157 /* OPTLogger.swift in Sources */, 6E75180122C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, - 98C2DF752FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */, 98F28A672E05220300A86546 /* CmabServiceTests.swift in Sources */, 98AC98472DB7B762001405DD /* BucketTests_HoldoutToVariation.swift in Sources */, 6E75175722C520D400B2B157 /* LogMessage.swift in Sources */, @@ -5207,7 +5196,6 @@ 6E75187922C520D400B2B157 /* Variation.swift in Sources */, 6E75191522C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */, 98C2DF242F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */, - 0921CDD1BC7F41F59B2D4CC3 /* FeatureGateTests_LocalHoldouts.swift in Sources */, 6E75195D22C520D500B2B157 /* OPTBucketer.swift in Sources */, 6E9B117622C5487100C22D81 /* DatafileHandlerTests.swift in Sources */, 84E2E97F2855875E001114AB /* OdpEventManager.swift in Sources */, @@ -5306,7 +5294,6 @@ 6E7516EC22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E75181A22C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, 6EF8DE2624BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, - 98C2DF782FA8D207003F2443 /* BaseHoldoutTests.swift in Sources */, 8464087E28130D3200CCF97D /* Integration.swift in Sources */, 6E9B119722C5488300C22D81 /* ConditionLeafTests.swift in Sources */, 6E75184A22C520D400B2B157 /* Event.swift in Sources */, @@ -5441,7 +5428,6 @@ 6E75193322C520D500B2B157 /* OPTDataStore.swift in Sources */, 84861811286D0B8900B7F41B /* OdpSegmentManagerTests.swift in Sources */, 6E7517EF22C520D400B2B157 /* DataStoreMemory.swift in Sources */, - 98C2DF762FA8D055003F2443 /* BaseHoldoutTests.swift in Sources */, 98F28A682E05220300A86546 /* CmabServiceTests.swift in Sources */, 6E75194B22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E75195722C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -5514,7 +5500,6 @@ 6E7516FD22C520D400B2B157 /* OptimizelyLogLevel.swift in Sources */, 6E75187322C520D400B2B157 /* Variation.swift in Sources */, 98C2DF252F900669003F2443 /* DecisionServiceTests_LocalHoldouts.swift in Sources */, - BB21E5C3E29D4F0C892E523C /* FeatureGateTests_LocalHoldouts.swift in Sources */, 6E7517E322C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75179922C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E9B115C22C5486E00C22D81 /* DatafileHandlerTests.swift in Sources */, @@ -5587,7 +5572,6 @@ 6E75193522C520D500B2B157 /* OPTDataStore.swift in Sources */, 6EC6DD4824ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75182122C520D400B2B157 /* BatchEventBuilder.swift in Sources */, - 98C2DF772FA8D1FF003F2443 /* BaseHoldoutTests.swift in Sources */, 983F81852F801E7500CDBC8D /* FeatureRolloutTests.swift in Sources */, 6E86CEA924FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E9B118322C5488100C22D81 /* UserAttributeTests_Evaluate.swift in Sources */, diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 7036d5cd..f2210635 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -658,20 +658,18 @@ class DefaultDecisionService: OPTDecisionService { } // check local holdouts targeting this rule - if FeatureGates.localHoldouts { - let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) - for holdout in localHoldouts { - let holdoutDecision = getVariationForHoldout(config: config, - flagKey: flagKey, - holdout: holdout, - user: user, - options: options) - reasons.merge(holdoutDecision.reasons) - if let variation = holdoutDecision.result { - // User is in holdout — return holdout variation immediately, skip this rule - let variationDecision = VariationDecision(variation: variation, holdout: holdout) - return DecisionResponse(result: variationDecision, reasons: reasons) - } + let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) + for holdout in localHoldouts { + let holdoutDecision = getVariationForHoldout(config: config, + flagKey: flagKey, + holdout: holdout, + user: user, + options: options) + reasons.merge(holdoutDecision.reasons) + if let variation = holdoutDecision.result { + // User is in holdout — return holdout variation immediately, skip this rule + let variationDecision = VariationDecision(variation: variation, holdout: holdout) + return DecisionResponse(result: variationDecision, reasons: reasons) } } @@ -718,20 +716,18 @@ class DefaultDecisionService: OPTDecisionService { } // check local holdouts targeting this delivery rule - if FeatureGates.localHoldouts { - let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) - for holdout in localHoldouts { - let holdoutDecision = getVariationForHoldout(config: config, - flagKey: flagKey, - holdout: holdout, - user: user, - options: options) - reasons.merge(holdoutDecision.reasons) - if let variation = holdoutDecision.result { - // User is in holdout — return holdout variation with holdout info - let decision = DeliveryRuleDecision(variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout) - return DecisionResponse(result: decision, reasons: reasons) - } + let localHoldouts = config.getHoldoutsForRule(ruleId: rule.id) + for holdout in localHoldouts { + let holdoutDecision = getVariationForHoldout(config: config, + flagKey: flagKey, + holdout: holdout, + user: user, + options: options) + reasons.merge(holdoutDecision.reasons) + if let variation = holdoutDecision.result { + // User is in holdout — return holdout variation with holdout info + let decision = DeliveryRuleDecision(variation: variation, skipToEveryoneElse: skipToEveryoneElse, holdout: holdout) + return DecisionResponse(result: decision, reasons: reasons) } } diff --git a/Sources/Utils/Constants.swift b/Sources/Utils/Constants.swift index db04f22c..0c106b2a 100644 --- a/Sources/Utils/Constants.swift +++ b/Sources/Utils/Constants.swift @@ -16,10 +16,6 @@ import Foundation -struct FeatureGates { - static var localHoldouts = false -} - struct Constants { struct Attributes { static let reservedBucketIdAttribute = "$opt_bucketing_id" diff --git a/Tests/OptimizelyTests-Common/BaseHoldoutTests.swift b/Tests/OptimizelyTests-Common/BaseHoldoutTests.swift deleted file mode 100644 index 4a64c1d3..00000000 --- a/Tests/OptimizelyTests-Common/BaseHoldoutTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright 2022, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -class BaseHoldoutTests: XCTestCase { - override func setUp() { - FeatureGates.localHoldouts = true - } - - override func tearDown() { - FeatureGates.localHoldouts = false - } -} diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift b/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift index 1f9bc14d..0470af28 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTest_Holdouts.swift @@ -16,7 +16,7 @@ import XCTest -class DecisionListenerTests_Holdouts: BaseHoldoutTests { +class DecisionListenerTests_Holdouts: XCTestCase { let kUserId = "11111" var optimizely: OptimizelyClient! var notificationCenter: OPTNotificationCenter! @@ -61,12 +61,13 @@ class DecisionListenerTests_Holdouts: BaseHoldoutTests { override func setUp() { super.setUp() + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, eventDispatcher: eventDispatcher, userProfileService: OTUtils.createClearUserProfileService()) - + try! optimizely.start(datafile: OTUtils.loadJSONDatafile("decide_datafile")!) - + var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout // Audience "13389130056" requires "country" = "US" holdout.audienceIds = ["13389130056"] @@ -78,10 +79,6 @@ class DecisionListenerTests_Holdouts: BaseHoldoutTests { self.notificationCenter = self.optimizely.notificationCenter! } - - override func tearDown() { - super.tearDown() - } func testDecisionListenerDecideWithUserInHoldout() { let exp = expectation(description: "x") diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift index 62907151..31f2e857 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_Holdouts.swift @@ -16,7 +16,7 @@ import XCTest -class DecisionServiceTests_Holdouts: BaseHoldoutTests { +class DecisionServiceTests_Holdouts: XCTestCase { var optimizely: OptimizelyClient! var config: ProjectConfig! @@ -189,6 +189,7 @@ class DecisionServiceTests_Holdouts: BaseHoldoutTests { override func setUp() { super.setUp() + self.optimizely = OTUtils.createOptimizely(datafileName: "empty_datafile", clearUserProfileService: true) self.config = self.optimizely.config! @@ -211,10 +212,6 @@ class DecisionServiceTests_Holdouts: BaseHoldoutTests { self.config.holdoutConfig.allHoldouts = [holdout] } - override func tearDown() { - super.tearDown() - } - } // MARK: - Test doesMeetAudienceConditions() diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift index f652b766..0d39787a 100644 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift +++ b/Tests/OptimizelyTests-Common/DecisionServiceTests_LocalHoldouts.swift @@ -19,7 +19,7 @@ import XCTest /// Integration tests for Local Holdouts functionality /// Tests that local holdouts are correctly evaluated at the rule level /// and global holdouts are evaluated at the flag level before any rules -class DecisionServiceTests_LocalHoldouts: BaseHoldoutTests { +class DecisionServiceTests_LocalHoldouts: XCTestCase { var optimizely: OptimizelyClient! var config: ProjectConfig! @@ -52,6 +52,7 @@ class DecisionServiceTests_LocalHoldouts: BaseHoldoutTests { override func setUp() { super.setUp() + // Load a real datafile for testing optimizely = OTUtils.createOptimizely(datafileName: "decide_datafile", clearUserProfileService: true) @@ -59,10 +60,6 @@ class DecisionServiceTests_LocalHoldouts: BaseHoldoutTests { decisionService = optimizely.decisionService as? DefaultDecisionService } - override func tearDown() { - super.tearDown() - } - // MARK: - Global Holdouts Tests func testGlobalHoldout_EvaluatedBeforeAllRules() { diff --git a/Tests/OptimizelyTests-Common/FeatureGateTests_LocalHoldouts.swift b/Tests/OptimizelyTests-Common/FeatureGateTests_LocalHoldouts.swift deleted file mode 100644 index 6081cef2..00000000 --- a/Tests/OptimizelyTests-Common/FeatureGateTests_LocalHoldouts.swift +++ /dev/null @@ -1,224 +0,0 @@ -// -// Copyright 2026, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -/// Tests to verify FeatureGates.localHoldouts flag behavior -/// These tests do NOT inherit from BaseHoldoutTests to control the flag state directly -class FeatureGateTests_LocalHoldouts: XCTestCase { - - var optimizely: OptimizelyClient! - var config: ProjectConfig! - - let userId = "test_user" - let flagKey = "feature_1" - let experimentRuleId = "10390977673" // From decide_datafile - let deliveryRuleId = "3332020515" // From decide_datafile - - var sampleHoldout: [String: Any] { - return [ - "status": "Running", - "id": "holdout_test_id", - "key": "holdout_test_key", - "trafficAllocation": [ - ["entityId": "holdout_variation_id", "endOfRange": 5000] // 50% traffic - ], - "audienceIds": [], - "variations": [ - [ - "variables": [], - "id": "holdout_variation_id", - "key": "holdout_variation_key", - "featureEnabled": false - ] - ] - ] - } - - override func setUp() { - super.setUp() - - optimizely = OTUtils.createOptimizely(datafileName: "decide_datafile", - clearUserProfileService: true) - config = optimizely.config! - } - - override func tearDown() { - // Always reset flag to false to prevent test pollution - FeatureGates.localHoldouts = false - super.tearDown() - } - - // MARK: - Flag OFF Tests (Local Holdouts Should Be Skipped) - - func testLocalHoldoutsSkippedWhenFlagOff_ExperimentRule() { - // Setup: Flag is OFF - FeatureGates.localHoldouts = false - - // Create local holdout targeting experiment rule - var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout - holdout.includedRules = [experimentRuleId] - config.project.holdouts = [holdout] - config.holdoutConfig.allHoldouts = [holdout] - - // Mock bucketer to ensure user WOULD bucket into holdout if it were evaluated - let mockBucketer = MockBucketer(mockBucketValue: 2500) // Within holdout range (0-5000) - let mockDecisionService = DefaultDecisionService( - userProfileService: OTUtils.createClearUserProfileService(), - bucketer: mockBucketer - ) - optimizely.decisionService = mockDecisionService - - // Execute decision - let user = optimizely.createUserContext(userId: userId) - let decision = user.decide(key: flagKey) - - // Verify: User did NOT get holdout variation (flag is off, so holdout skipped) - XCTAssertNotEqual(decision.variationKey, "holdout_variation_key", - "Local holdout should be skipped when FeatureGates.localHoldouts = false") - XCTAssertNotEqual(decision.ruleKey, "holdout_test_key", - "Should get experiment rule, not holdout") - } - - func testLocalHoldoutsSkippedWhenFlagOff_DeliveryRule() { - // Setup: Flag is OFF - FeatureGates.localHoldouts = false - - // Create local holdout targeting delivery rule - var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout - holdout.includedRules = [deliveryRuleId] - config.project.holdouts = [holdout] - config.holdoutConfig.allHoldouts = [holdout] - - // Mock bucketer to ensure user WOULD bucket into holdout if it were evaluated - let mockBucketer = MockBucketer(mockBucketValue: 2500) - let mockDecisionService = DefaultDecisionService( - userProfileService: OTUtils.createClearUserProfileService(), - bucketer: mockBucketer - ) - optimizely.decisionService = mockDecisionService - - // Execute decision - let user = optimizely.createUserContext(userId: userId) - let decision = user.decide(key: flagKey) - - // Verify: User did NOT get holdout variation - XCTAssertNotEqual(decision.variationKey, "holdout_variation_key", - "Local holdout should be skipped when FeatureGates.localHoldouts = false") - XCTAssertNotEqual(decision.ruleKey, "holdout_test_key", - "Should get delivery rule, not holdout") - } - - // MARK: - Flag ON Tests (Local Holdouts Should Be Evaluated) - - func testLocalHoldoutsEvaluatedWhenFlagOn_ExperimentRule() { - // Setup: Flag is ON - FeatureGates.localHoldouts = true - - // Create local holdout targeting experiment rule - var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout - holdout.includedRules = [experimentRuleId] - config.project.holdouts = [holdout] - config.holdoutConfig.allHoldouts = [holdout] - - // Mock bucketer to bucket user into holdout - let mockBucketer = MockBucketer(mockBucketValue: 2500) - let mockDecisionService = DefaultDecisionService( - userProfileService: OTUtils.createClearUserProfileService(), - bucketer: mockBucketer - ) - optimizely.decisionService = mockDecisionService - - // Execute decision - let user = optimizely.createUserContext(userId: userId) - let decision = user.decide(key: flagKey) - - // Verify: User DID get holdout variation (flag is on) - XCTAssertEqual(decision.variationKey, "holdout_variation_key", - "Local holdout should be evaluated when FeatureGates.localHoldouts = true") - XCTAssertEqual(decision.ruleKey, "holdout_test_key", - "Should get holdout, not experiment rule") - XCTAssertFalse(decision.enabled, "Holdout variation has featureEnabled: false") - } - - func testLocalHoldoutsEvaluatedWhenFlagOn_DeliveryRule() { - // Setup: Flag is ON - FeatureGates.localHoldouts = true - - // Create local holdout targeting delivery rule - var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout - holdout.includedRules = [deliveryRuleId] - config.project.holdouts = [holdout] - config.holdoutConfig.allHoldouts = [holdout] - - // Mock bucketer to bucket user into holdout - let mockBucketer = MockBucketer(mockBucketValue: 2500) - let mockDecisionService = DefaultDecisionService( - userProfileService: OTUtils.createClearUserProfileService(), - bucketer: mockBucketer - ) - optimizely.decisionService = mockDecisionService - - // Execute decision - let user = optimizely.createUserContext(userId: userId) - let decision = user.decide(key: flagKey) - - // Verify: User DID get holdout variation (flag is on) - XCTAssertEqual(decision.variationKey, "holdout_variation_key", - "Local holdout should be evaluated when FeatureGates.localHoldouts = true") - XCTAssertEqual(decision.ruleKey, "holdout_test_key", - "Should get holdout, not delivery rule") - XCTAssertFalse(decision.enabled, "Holdout variation has featureEnabled: false") - } - - // MARK: - Global Holdouts (Flag State Should Not Matter) - - func testGlobalHoldoutsWorkRegardlessOfFlagState() { - // Create global holdout (no includedRules) - var holdout = try! OTUtils.model(from: sampleHoldout) as Holdout - holdout.includedRules = nil // Global holdout - config.project.holdouts = [holdout] - config.holdoutConfig.allHoldouts = [holdout] - - // Mock bucketer to bucket user into holdout - let mockBucketer = MockBucketer(mockBucketValue: 2500) - let mockDecisionService = DefaultDecisionService( - userProfileService: OTUtils.createClearUserProfileService(), - bucketer: mockBucketer - ) - optimizely.decisionService = mockDecisionService - - // Test with flag OFF - FeatureGates.localHoldouts = false - let user1 = optimizely.createUserContext(userId: userId) - let decision1 = user1.decide(key: flagKey) - - XCTAssertEqual(decision1.variationKey, "holdout_variation_key", - "Global holdout should work when flag is OFF") - XCTAssertEqual(decision1.ruleKey, "holdout_test_key", - "Should get global holdout regardless of flag state") - - // Test with flag ON - FeatureGates.localHoldouts = true - let user2 = optimizely.createUserContext(userId: userId + "_2") - let decision2 = user2.decide(key: flagKey) - - XCTAssertEqual(decision2.variationKey, "holdout_variation_key", - "Global holdout should work when flag is ON") - XCTAssertEqual(decision2.ruleKey, "holdout_test_key", - "Should get global holdout regardless of flag state") - } -} diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift index b4993be6..a21ca3c0 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_Holdouts.swift @@ -16,7 +16,7 @@ import XCTest -class OptimizelyUserContextTests_Decide_Holdouts: BaseHoldoutTests { +class OptimizelyUserContextTests_Decide_Holdouts: XCTestCase { let kUserId = "tester" var optimizely: OptimizelyClient! var eventDispatcher = MockEventDispatcher() @@ -45,16 +45,13 @@ class OptimizelyUserContextTests_Decide_Holdouts: BaseHoldoutTests { override func setUp() { super.setUp() + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, eventDispatcher: eventDispatcher, userProfileService: OTUtils.createClearUserProfileService()) - + try! optimizely.start(datafile: OTUtils.loadJSONDatafile("decide_datafile")!) } - - override func tearDown() { - super.tearDown() - } func test_decide_with_global_holdout_audience_matched() { let featureKey = "feature_1" diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift index 6e57e7bd..f05d7ba0 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide_With_Holdouts_Reasons.swift @@ -16,7 +16,7 @@ import XCTest -class OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: BaseHoldoutTests { +class OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: XCTestCase { let kUserId = "tester" var optimizely: OptimizelyClient! @@ -44,16 +44,12 @@ class OptimizelyUserContextTests_Decide_With_Holdouts_Reasons: BaseHoldoutTests override func setUp() { super.setUp() - + optimizely = OptimizelyClient(sdkKey: OTUtils.randomSdkKey, userProfileService: OTUtils.createClearUserProfileService()) - + try! optimizely.start(datafile: OTUtils.loadJSONDatafile("decide_datafile")!) } - - override func tearDown() { - super.tearDown() - } /// Test when user is bucketed into the global holdout func testDecideReasons_userBucketedIntoGlobalHoldout() {