Skip to content

Commit 167f579

Browse files
jaeoptthomaszurkan-optimizely
authored andcommitted
add test cases for DecisionService (#115)
* add more tests for DecisionService (wip) * fix error messages * add tests for DecisionServices * fix all per Tom's refactoring on decisionService * fix merge errors * fix tests for redundant groups * add logger to DecisionService * fix merge error * remove unused datafiles ported from ObjC tests
1 parent bfcd2cc commit 167f579

26 files changed

+1138
-1166
lines changed

OptimizelySDK/Data Model/Audience/AttributeValue.swift

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,29 @@
88

99
import Foundation
1010

11-
enum AttributeValue: Codable, Equatable {
11+
enum AttributeValue: Codable, Equatable, CustomStringConvertible {
1212
case string(String)
1313
case int(Int64) // supported value range [-2^53, 2^53]
1414
case double(Double)
1515
case bool(Bool)
1616
// not defined in datafile schema, but required for forward compatiblity (see Nikhil's doc)
1717
case others
1818

19+
var description: String {
20+
switch self {
21+
case .string(let value):
22+
return "string(\(value))"
23+
case .double(let value):
24+
return "double(\(value))"
25+
case .int(let value):
26+
return "int(\(value))"
27+
case .bool(let value):
28+
return "bool(\(value))"
29+
case .others:
30+
return "others"
31+
}
32+
}
33+
1934
init?(value: Any?) {
2035

2136
guard let value = value else { return nil }
@@ -94,15 +109,15 @@ enum AttributeValue: Codable, Equatable {
94109

95110
extension AttributeValue {
96111

97-
func isExactMatch(with target: Any?) throws -> Bool {
112+
func isExactMatch(with target: Any) throws -> Bool {
98113
try checkValidAttributeNumber(target)
99114

100115
guard let targetValue = AttributeValue(value: target) else {
101-
throw OptimizelyError.conditionInvalidValueType(#function)
116+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
102117
}
103118

104119
guard self.isComparable(with: targetValue) else {
105-
throw OptimizelyError.conditionInvalidValueType(#function)
120+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
106121
}
107122

108123
// same type and same value
@@ -118,49 +133,49 @@ extension AttributeValue {
118133
return false
119134
}
120135

121-
func isSubstring(of target: Any?) throws -> Bool {
136+
func isSubstring(of target: Any) throws -> Bool {
122137
guard case .string(let value) = self else {
123-
throw OptimizelyError.conditionInvalidValueType(#function)
138+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
124139
}
125140

126-
guard let target = target as? String else {
127-
throw OptimizelyError.conditionInvalidValueType(#function)
141+
guard let targetStr = target as? String else {
142+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
128143
}
129144

130-
return target.contains(value)
145+
return targetStr.contains(value)
131146
}
132147

133-
func isGreater(than target: Any?) throws -> Bool {
148+
func isGreater(than target: Any) throws -> Bool {
134149
try checkValidAttributeNumber(target)
135150

136151
guard let targetValue = AttributeValue(value: target) else {
137-
throw OptimizelyError.conditionInvalidValueType(#function)
152+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
138153
}
139154

140155
guard let currentDouble = self.doubleValue else {
141-
throw OptimizelyError.conditionInvalidValueType(#function)
156+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
142157
}
143158

144159
guard let targetDouble = targetValue.doubleValue else {
145-
throw OptimizelyError.conditionInvalidValueType(#function)
160+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
146161
}
147162

148163
return currentDouble > targetDouble
149164
}
150165

151-
func isLess(than target: Any?) throws -> Bool {
166+
func isLess(than target: Any) throws -> Bool {
152167
try checkValidAttributeNumber(target)
153168

154169
guard let targetValue = AttributeValue(value: target) else {
155-
throw OptimizelyError.conditionInvalidValueType(#function)
170+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
156171
}
157172

158173
guard let currentDouble = self.doubleValue else {
159-
throw OptimizelyError.conditionInvalidValueType(#function)
174+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
160175
}
161176

162177
guard let targetDouble = targetValue.doubleValue else {
163-
throw OptimizelyError.conditionInvalidValueType(#function)
178+
throw OptimizelyError.conditionInvalidValueType(prettySrc(#function, target: target))
164179
}
165180

166181
return currentDouble < targetDouble
@@ -186,9 +201,7 @@ extension AttributeValue {
186201
}
187202
}
188203

189-
func checkValidAttributeNumber(_ number: Any?) throws {
190-
guard let number = number else { return }
191-
204+
func checkValidAttributeNumber(_ number: Any) throws {
192205
var num: Double
193206

194207
if let number = Utils.getInt64Value(number) {
@@ -206,5 +219,9 @@ extension AttributeValue {
206219
}
207220
}
208221

222+
func prettySrc(_ src: String, target: Any? = nil) -> String {
223+
return "(\(src)): \(self) target: " + (target != nil ? "\(target!)" : "nil")
224+
}
225+
209226
}
210227

OptimizelySDK/Data Model/Audience/UserAttribute.swift

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ struct UserAttribute: Codable, Equatable {
8484
}
8585
}
8686

87+
// MARK: - Evaluate
88+
8789
extension UserAttribute {
8890

8991
func evaluate(attributes: OptimizelyAttributes?) throws -> Bool {
@@ -102,29 +104,35 @@ extension UserAttribute {
102104
throw OptimizelyError.conditionInvalidFormat("empty name in condition")
103105
}
104106

105-
if matchFinal != .exists, value == nil {
106-
throw OptimizelyError.conditionInvalidFormat("missing value (\(nameFinal)) in condition)")
107-
}
108-
109107
let attributes = attributes ?? OptimizelyAttributes()
110108

111109
let attributeValue = attributes[nameFinal] ?? nil // default to nil to avoid warning "coerced from 'Any??' to 'Any?'"
112110

111+
if matchFinal != .exists {
112+
if value == nil {
113+
throw OptimizelyError.conditionInvalidFormat("missing value (\(nameFinal)) in condition)")
114+
}
115+
116+
if attributeValue == nil {
117+
throw OptimizelyError.conditionNoAttributeValue("no attribute value for (\(nameFinal))")
118+
}
119+
}
120+
113121
switch matchFinal {
114122
case .exists:
115123
return !(attributeValue is NSNull || attributeValue == nil)
116124
case .exact:
117-
return try value!.isExactMatch(with: attributeValue)
125+
return try value!.isExactMatch(with: attributeValue!)
118126
case .substring:
119-
return try value!.isSubstring(of: attributeValue)
127+
return try value!.isSubstring(of: attributeValue!)
120128
case .lt:
121129
// user attribute "less than" this condition value
122130
// so evaluate if this condition value "isGreater" than the user attribute value
123-
return try value!.isGreater(than: attributeValue)
131+
return try value!.isGreater(than: attributeValue!)
124132
case .gt:
125133
// user attribute "greater than" this condition value
126134
// so evaluate if this condition value "isLess" than the user attribute value
127-
return try value!.isLess(than: attributeValue)
135+
return try value!.isLess(than: attributeValue!)
128136
}
129137
}
130138

OptimizelySDK/Data Model/DispatchEvents/BatchEvent.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,6 @@ struct DispatchEvent: Codable, Equatable {
8585
var revenue: AttributeValue?
8686
var value: AttributeValue?
8787

88-
// TODO: [Tom] these two in spec, not used here. is it ok?
89-
// quantity: MISSING
90-
// type: MISSING
91-
9288
enum CodingKeys: String, CodingKey {
9389
case entityID = "entity_id"
9490
case key

OptimizelySDK/Data Model/Experiment.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ struct Experiment: Codable, Equatable {
3838
var forcedVariations: [String: String]
3939
}
4040

41+
// MARK: - Utils
42+
4143
extension Experiment {
4244

4345
func getVariation(id: String) -> Variation? {
@@ -47,5 +49,9 @@ extension Experiment {
4749
func getVariation(key: String) -> Variation? {
4850
return variations.filter { $0.key == key }.first
4951
}
52+
53+
var isActivated: Bool {
54+
return status == .running
55+
}
5056

5157
}

OptimizelySDK/Data Model/FeatureFlag.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ struct FeatureFlag: Codable, Equatable {
2222
var experimentIds: [String]
2323
var rolloutId: String
2424
var variables: [FeatureVariable]
25+
}
26+
27+
// MARK: - Utils
28+
29+
extension FeatureFlag {
30+
31+
func getVariable(key: String) -> FeatureVariable? {
32+
return variables.filter { $0.key == key }.first
33+
}
2534

26-
// TODO: this will be removed. clean up existing code using this
27-
//var groupId: String?
2835
}

OptimizelySDK/Data Model/Group.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,13 @@ struct Group: Codable, Equatable {
3030
var trafficAllocation: [TrafficAllocation]
3131
var experiments: [Experiment]
3232
}
33+
34+
// MARK: - Utils
35+
36+
extension Group {
37+
38+
func getExperiemnt(id: String) -> Experiment? {
39+
return experiments.filter { $0.id == id }.first
40+
}
41+
42+
}

OptimizelySDK/Data Model/Project.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,24 @@ struct Project: Codable, Equatable {
3939
extension Project: ProjectProtocol {
4040

4141
func evaluateAudience(audienceId: String, attributes: OptimizelyAttributes?) throws -> Bool {
42-
let audienceMatch = typedAudiences?.filter{$0.id == audienceId}.first ??
43-
audiences.filter{$0.id == audienceId}.first
44-
45-
guard let audience = audienceMatch else {
42+
guard let audience = getAudience(id: audienceId) else {
4643
throw OptimizelyError.conditionNoMatchingAudience(audienceId)
4744
}
4845

4946
return try audience.evaluate(project: self, attributes: attributes)
5047
}
5148

5249
}
50+
51+
// MARK: - Utils
52+
53+
extension Project {
54+
55+
func getAudience(id: String) -> Audience? {
56+
return typedAudiences?.filter{ $0.id == id }.first ??
57+
audiences.filter{ $0.id == id }.first
58+
}
59+
60+
}
61+
62+

OptimizelySDK/Data Model/ProjectConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ extension ProjectConfig {
168168
* Get an audience for a given audience id.
169169
*/
170170
func getAudience(id: String) -> Audience? {
171-
return project.audiences.filter{ $0.id == id }.first
171+
return project.getAudience(id: id)
172172
}
173173

174174
/**

OptimizelySDK/Data Model/Variation.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@ struct Variation: Codable, Equatable {
2626
var featureEnabled: Bool?
2727
var variables: [Variable]?
2828
}
29+
30+
// MARK: - Utils
31+
32+
extension Variation {
33+
34+
func getVariable(id: String) -> Variable? {
35+
return variables?.filter { $0.id == id }.first
36+
}
37+
38+
}

OptimizelySDK/Extensions/Array+Extension.swift

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,32 @@ extension Array where Element == ThrowableCondition {
2323
}
2424

2525
for eval in self {
26-
if try eval() == false {
27-
return false
26+
do {
27+
if try eval() == false {
28+
return false
29+
}
30+
} catch let error as OptimizelyError {
31+
throw OptimizelyError.conditionInvalidFormat("AND with invalid items [\(error.reason)]")
2832
}
2933
}
3034

3135
return true
3236
}
3337

34-
// return trye if any item is true (even with other error items)
38+
// return try if any item is true (even with other error items)
3539
func or() throws -> Bool {
36-
var foundError = false
40+
var foundError: OptimizelyError? = nil
3741

3842
for eval in self {
3943
do {
4044
if try eval() { return true }
41-
} catch {
42-
foundError = true
45+
} catch let error as OptimizelyError {
46+
foundError = error
4347
}
4448
}
4549

46-
if foundError {
47-
throw OptimizelyError.conditionInvalidFormat("logical OR with invalid items")
50+
if foundError != nil {
51+
throw OptimizelyError.conditionInvalidFormat("OR with invalid items [\(foundError!.reason)]")
4852
}
4953

5054
return false
@@ -56,7 +60,11 @@ extension Array where Element == ThrowableCondition {
5660
throw OptimizelyError.conditionInvalidFormat(#function)
5761
}
5862

59-
let result = try eval()
60-
return !result
63+
do {
64+
let result = try eval()
65+
return !result
66+
} catch let error as OptimizelyError {
67+
throw OptimizelyError.conditionInvalidFormat("NOT with invalid items [\(error.reason)]")
68+
}
6169
}
6270
}

0 commit comments

Comments
 (0)