Skip to content

Commit f310734

Browse files
cleanup: add unit test cases
1 parent 9e1669c commit f310734

File tree

4 files changed

+122
-66
lines changed

4 files changed

+122
-66
lines changed

Sources/Data Model/HoldoutConfig.swift

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,34 @@ import Foundation
1919
struct HoldoutConfig {
2020
var allHoldouts: [Holdout] {
2121
didSet {
22-
updateHoldoutProperties()
22+
updateHoldoutMapping()
2323
}
2424
}
25-
private(set) var holdoutIdMap: [String: Holdout] = [:]
2625
private(set) var global: [Holdout] = []
26+
private(set) var holdoutIdMap: [String: Holdout] = [:]
27+
private(set) var flagHoldoutsMap: [String: [Holdout]] = [:]
2728
private(set) var includedHoldouts: [String: [Holdout]] = [:]
2829
private(set) var excludedHoldouts: [String: [Holdout]] = [:]
29-
private(set) var flagHoldoutsMap: [String: [Holdout]] = [:]
3030

31-
let logger = OPTLoggerFactory.getLogger()
31+
var cacheWriteCount = 0 // for testing purpose only
32+
33+
var logger: OPTLogger = OPTLoggerFactory.getLogger()
3234

3335
init(allholdouts: [Holdout] = []) {
3436
self.allHoldouts = allholdouts
35-
updateHoldoutProperties()
37+
updateHoldoutMapping()
3638
}
3739

38-
mutating func updateHoldoutProperties() {
40+
/// Updates internal mappings of holdouts including the id map, global list, and per-flag inclusion/exclusion maps.
41+
mutating func updateHoldoutMapping() {
3942
holdoutIdMap = {
4043
var map = [String: Holdout]()
4144
allHoldouts.forEach { map[$0.id] = $0 }
4245
return map
4346
}()
47+
4448
flagHoldoutsMap = [:]
4549
global = []
46-
4750
includedHoldouts = [:]
4851
excludedHoldouts = [:]
4952

@@ -80,10 +83,14 @@ struct HoldoutConfig {
8083
}
8184
}
8285

86+
/// Returns the applicable holdouts for the given flag ID by combining global holdouts (excluding any specified) and included holdouts, in that order.
87+
/// Caches the result for future calls.
88+
/// - Parameter id: The flag identifier.
89+
/// - Returns: An array of `Holdout` objects relevant to the given flag.
8390
mutating func getHoldoutForFlag(id: String) -> [Holdout] {
8491
guard !allHoldouts.isEmpty else { return [] }
8592

86-
// Check cache and return if persist holdouts
93+
// Check cache and return persistent holdouts
8794
if let holdouts = flagHoldoutsMap[id] {
8895
return holdouts
8996
}
@@ -105,10 +112,12 @@ struct HoldoutConfig {
105112
activeHoldouts += includedHoldouts
106113

107114
flagHoldoutsMap[id] = activeHoldouts
115+
cacheWriteCount += 1
108116

109117
return flagHoldoutsMap[id] ?? []
110118
}
111119

120+
/// Get a Holdout object for an Id.
112121
func getHoldout(id: String) -> Holdout? {
113122
return holdoutIdMap[id]
114123
}

Sources/Data Model/ProjectConfig.swift

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -162,33 +162,7 @@ class ProjectConfig {
162162
func getHoldoutForFlag(id: String) -> [Holdout] {
163163
return holdoutConfig.getHoldoutForFlag(id: id)
164164
}
165-
166-
// private func updateHoldoutsMapForFlag(id: String) {
167-
// if let included = inlcudedHoldoutsMap[id] {
168-
// return globalHoldouts + included
169-
// }
170-
//
171-
//// for holdout in project.holdouts {
172-
//// switch (holdout.includedFlags.isEmpty, holdout.excludedFlags.isEmpty) {
173-
//// case (true, true):
174-
//// // Global holdout
175-
//// global.append(holdout.id)
176-
////
177-
//// case (false, _):
178-
//// if holdout.includedFlags.contains(id) {
179-
//// others.append(holdout.id)
180-
//// }
181-
////
182-
//// case (_, false):
183-
//// if !holdout.excludedFlags.contains(id) {
184-
//// others.append(holdout.id)
185-
//// }
186-
//// }
187-
//// }
188-
////
189-
// flagHoldoutsMap[id] = globalHoldouts + others
190-
// }
191-
165+
192166
func getAllRulesForFlag(_ flag: FeatureFlag) -> [Experiment] {
193167
var rules = flag.experimentIds.compactMap { experimentIdMap[$0] }
194168
let rollout = self.rolloutIdMap[flag.rolloutId]

Tests/OptimizelyTests-DataModel/HoldoutConfigTests.swift

Lines changed: 103 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@
1717
import XCTest
1818

1919
class HoldoutConfigTests: XCTestCase {
20+
func testEmptyHoldouts_shouldHaveEmptyMaps() {
21+
let config = HoldoutConfig(allholdouts: [])
22+
23+
XCTAssertTrue(config.holdoutIdMap.isEmpty)
24+
XCTAssertTrue(config.global.isEmpty)
25+
XCTAssertTrue(config.includedHoldouts.isEmpty)
26+
XCTAssertTrue(config.excludedHoldouts.isEmpty)
27+
}
2028

2129
func testHoldoutMap() {
2230
let holdout0: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
2331
let holdout1: Holdout = try! OTUtils.model(from: HoldoutTests.sampleDataWithIncludedFlags)
2432
let holdout2: Holdout = try! OTUtils.model(from: HoldoutTests.sampleDataWithExcludedFlags)
2533

2634
let allHoldouts = [holdout0, holdout1, holdout2]
27-
28-
2935
let holdoutConfig = HoldoutConfig(allholdouts: allHoldouts)
3036

3137
XCTAssertEqual(holdoutConfig.holdoutIdMap["11111"]?.includedFlags, [])
@@ -37,7 +43,6 @@ class HoldoutConfigTests: XCTestCase {
3743
XCTAssertEqual(holdoutConfig.holdoutIdMap["3333"]?.includedFlags, [])
3844
XCTAssertEqual(holdoutConfig.holdoutIdMap["3333"]?.excludedFlags, ["8888", "9999"])
3945

40-
4146
XCTAssertEqual(holdoutConfig.global, [holdout0, holdout2])
4247

4348
XCTAssertEqual(holdoutConfig.includedHoldouts["4444"], [holdout1])
@@ -48,16 +53,12 @@ class HoldoutConfigTests: XCTestCase {
4853
func testGetHoldoutById() {
4954
var holdout0: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
5055
holdout0.id = "00000"
51-
5256
var holdout1: Holdout = try! OTUtils.model(from: HoldoutTests.sampleDataWithIncludedFlags)
5357
holdout1.id = "11111"
54-
5558
var holdout2: Holdout = try! OTUtils.model(from: HoldoutTests.sampleDataWithExcludedFlags)
5659
holdout2.id = "22222"
5760

5861
let allHoldouts = [holdout0, holdout1, holdout2]
59-
60-
6162
let holdoutConfig = HoldoutConfig(allholdouts: allHoldouts)
6263

6364
XCTAssertEqual(holdoutConfig.getHoldout(id: "00000"), holdout0)
@@ -66,47 +67,118 @@ class HoldoutConfigTests: XCTestCase {
6667

6768
}
6869

69-
func testGetHoldoutForFlag() {
70+
func testHoldoutOrdering_globalThenIncluded() {
71+
var global1: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
72+
global1.id = "g1"
73+
74+
var global2: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
75+
global2.id = "g2"
7076

77+
var included: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
78+
included.id = "i1"
79+
included.includedFlags = ["f"]
80+
81+
var config = HoldoutConfig(allholdouts: [included, global1, global2])
82+
83+
let result = config.getHoldoutForFlag(id: "f").map(\.id)
84+
XCTAssertEqual(result, ["g1", "g2", "i1"])
85+
}
86+
87+
func testHoldoutOrdering_with_Both_IncludedAndExcludedFlags() {
7188
let flag1 = "11111"
7289
let flag2 = "22222"
7390
let flag3 = "33333"
7491
let flag4 = "44444"
7592

76-
var holdout1: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
77-
holdout1.id = "11"
78-
holdout1.includedFlags = [flag1]
79-
holdout1.excludedFlags = []
93+
var inc: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
94+
inc.id = "i1"
95+
inc.includedFlags = [flag1]
8096

81-
var holdout2: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
82-
holdout2.id = "22"
83-
holdout2.includedFlags = []
84-
holdout2.excludedFlags = [flag2]
97+
var exc: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
98+
exc.id = "e1"
99+
exc.excludedFlags = [flag2]
85100

86-
var holdout3: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
87-
holdout3.id = "33"
88-
holdout3.includedFlags = []
89-
holdout3.excludedFlags = []
101+
var gh1: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
102+
gh1.id = "gh1"
103+
gh1.includedFlags = []
104+
gh1.excludedFlags = []
90105

91-
var holdout4: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
92-
holdout4.id = "44"
93-
holdout4.includedFlags = []
94-
holdout4.excludedFlags = []
106+
var gh2: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
107+
gh2.id = "gh2"
108+
gh2.includedFlags = []
109+
gh2.excludedFlags = []
95110

96111

97-
let allHoldouts = [holdout1, holdout2, holdout3, holdout4]
112+
let allHoldouts = [inc, exc, gh1, gh2]
98113
var holdoutConfig = HoldoutConfig(allholdouts: allHoldouts)
99114

100-
// Should maintain order. Global first then lcoal
115+
XCTAssertEqual(holdoutConfig.getHoldoutForFlag(id: flag1), [exc, gh1, gh2, inc])
116+
XCTAssertEqual(holdoutConfig.getHoldoutForFlag(id: flag2), [gh1, gh2])
101117

102-
XCTAssertEqual(holdoutConfig.getHoldoutForFlag(id: flag1), [holdout2, holdout3, holdout4, holdout1])
103-
XCTAssertEqual(holdoutConfig.getHoldoutForFlag(id: flag2), [holdout3, holdout4])
118+
XCTAssertEqual(holdoutConfig.getHoldoutForFlag(id: flag3), [exc, gh1, gh2])
119+
XCTAssertEqual(holdoutConfig.getHoldoutForFlag(id: flag4), [exc, gh1, gh2])
104120

105-
XCTAssertEqual(holdoutConfig.getHoldoutForFlag(id: flag3), [holdout2, holdout3, holdout4])
106-
XCTAssertEqual(holdoutConfig.getHoldoutForFlag(id: flag4), [holdout2, holdout3, holdout4])
107-
108121
}
109122

123+
func testExcludedHoldout_shouldNotAppearInGlobalForFlag() {
124+
var global: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
125+
global.id = "global"
126+
127+
var excluded: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
128+
excluded.id = "excluded"
129+
excluded.excludedFlags = ["f"]
130+
131+
var config = HoldoutConfig(allholdouts: [global, excluded])
132+
133+
let result = config.getHoldoutForFlag(id: "f").map(\.id)
134+
XCTAssertEqual(result, ["global"]) // excluded should not appear
135+
}
110136

137+
func testGetHoldoutForFlag_shouldUseCacheOnSecondCall() {
138+
var holdout: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
139+
holdout.id = "1"
140+
holdout.excludedFlags = ["f1"]
141+
142+
var config = HoldoutConfig(allholdouts: [holdout])
143+
144+
_ = config.getHoldoutForFlag(id: "f1")
145+
let writeCountAfterFirst = config.cacheWriteCount
146+
147+
_ = config.getHoldoutForFlag(id: "f1")
148+
let writeCountAfterSecond = config.cacheWriteCount
149+
150+
XCTAssertEqual(writeCountAfterFirst, 1)
151+
XCTAssertEqual(writeCountAfterSecond, 1, "Second call should not increase cache write count")
152+
}
111153

154+
func testHoldoutWithBothIncludedAndExcludedFlags_shouldLogError() {
155+
class ConfigMockLogger: OPTLogger {
156+
static var logLevel: OptimizelyLogLevel = .info
157+
var expectedLog: String = ""
158+
var logFound = false
159+
160+
required init() {}
161+
162+
func log(level: OptimizelyLogLevel, message: String) {
163+
if (message == expectedLog) {
164+
logFound = true
165+
}
166+
}
167+
}
168+
169+
var holdout: Holdout = try! OTUtils.model(from: HoldoutTests.sampleData)
170+
holdout.id = "invalid"
171+
holdout.includedFlags = ["f1"]
172+
holdout.excludedFlags = ["f2"]
173+
174+
let mockLogger = ConfigMockLogger()
175+
mockLogger.expectedLog = LogMessage.holdoutToFlagMappingError.description
176+
177+
var config = HoldoutConfig(allholdouts: [])
178+
config.logger = mockLogger
179+
180+
config.allHoldouts = [holdout]
181+
182+
XCTAssertTrue(mockLogger.logFound)
183+
}
112184
}

Tests/OptimizelyTests-DataModel/ProjectConfigTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class ProjectConfigTests: XCTestCase {
9898
var holdout2 = HoldoutTests.sampleData
9999
var holdout3 = HoldoutTests.sampleData
100100
var holdout4 = HoldoutTests.sampleData
101+
101102
holdout0["id"] = "3000" // Global holdout (no included or excluded flags)
102103
holdout1["id"] = "3001" // Global holdout (no included or excluded flags)
103104
holdout2["id"] = "3002" // Global holdout (no included or excluded flags)

0 commit comments

Comments
 (0)