• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

optimizely / swift-sdk / 1191

pending completion
1191

Pull #273

travis-ci-com

web-flow
fix start_simulator script to filter out minor iOS version of simulator
Pull Request #273: add/fix tests for EP+ED refactor

1602 of 1602 new or added lines in 26 files covered. (100.0%)

10207 of 10511 relevant lines covered (97.11%)

161.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.96
/Tests/TestUtils/OTUtils.swift
1
/****************************************************************************
2
 * Copyright 2019, Optimizely, Inc. and contributors                        *
3
 *                                                                          *
4
 * Licensed under the Apache License, Version 2.0 (the "License");          *
5
 * you may not use this file except in compliance with the License.         *
6
 * You may obtain a copy of the License at                                  *
7
 *                                                                          *
8
 *    http://www.apache.org/licenses/LICENSE-2.0                            *
9
 *                                                                          *
10
 * Unless required by applicable law or agreed to in writing, software      *
11
 * distributed under the License is distributed on an "AS IS" BASIS,        *
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13
 * See the License for the specific language governing permissions and      *
14
 * limitations under the License.                                           *
15
 ***************************************************************************/
16

17
import Foundation
18
import XCTest
19

20
@objc public class OTUtils: NSObject {
21
    
22
    @objc public static func clearRegistryService() {
23
        HandlerRegistryService.shared.removeAll()
24
    }
25
   
26
    static func isEqualWithEncodeThenDecode<T: Codable & Equatable>(_ model: T) -> Bool {
27
        let jsonData = try! JSONEncoder().encode(model)
28
        let modelExp = try! JSONDecoder().decode(T.self, from: jsonData)
29
        return modelExp == model
30
    }
31
    
32
    static func getAttributeValueFromNative(_ value: Any?) throws -> AttributeValue {
33
        // JSONEncoder does not support fragmented JSON format (string alone), so wrap in an array
34
        let json: [Any?] = [value]
35
        let jsonData = try JSONSerialization.data(withJSONObject: json, options: [])
36
        let modelArray = try JSONDecoder().decode([AttributeValue].self, from: jsonData)
37
        return modelArray[0]
38
    }
39
    
40
    static func jsonDataFromNative(_ raw: Any) -> Data {
41
        return try! JSONSerialization.data(withJSONObject: raw, options: [])
42
    }
43
    
44
    static func jsonStringFromNative(_ raw: Any) -> String {
45
        return String(data: jsonDataFromNative(raw), encoding: .utf8)!
46
    }
47
    
48
    static func model<T: Codable>(from raw: Any) throws -> T {
540✔
49
        return try JSONDecoder().decode(T.self, from: jsonDataFromNative(raw))
540✔
50
    }
540✔
51
    
52
    static func model<T: Codable>(fromData data: Data) throws -> T {
54✔
53
        return try JSONDecoder().decode(T.self, from: data)
54✔
54
    }
54✔
55
    
56
    static func loadJSONDatafile(_ filename: String) -> Data? {
57
        guard let filePath = Bundle(for: self).path(forResource: filename, ofType: "json") else {
58
            return nil
59
        }
60
        
61
        do {
62
            let fileContents = try String(contentsOfFile: filePath)
63
            return fileContents.data(using: .utf8)
64
        } catch {
65
            return nil
66
        }
67
    }
68
    
69
    static func createClearUserProfileService() -> DefaultUserProfileService {
420✔
70
        let ups = DefaultUserProfileService()
420✔
71
        ups.reset()
420✔
72
        return ups
420✔
73
    }
420✔
74
    
75
    // use EventProcessor + EventDispatcher
76
    static func createOptimizely(sdkKey: String? = nil,
77
                                 datafileName: String,
78
                                 clearUserProfileService: Bool,
79
                                 eventProcessor: OPTEventsProcessor? = nil,
80
                                 eventDispatcher: OPTEventsDispatcher? = nil) -> OptimizelyClient? {
81
        
82
        prepareDocumentFolderInSimulator()
83
        
84
        // use random sdkKey to avoid registration conflicts when multiple tests running in parallel
85
        let sdkKey = sdkKey ?? randomSdkKey
86
        
87
        //-------------------------------------------------------------------
88
        // reset previous services so that new EP (No SDKKey) can be registered OK
89
        //-------------------------------------------------------------------
90
        OTUtils.clearRegistryService()
91

92
        let userProfileService = clearUserProfileService ? createClearUserProfileService() : nil
93
        
94
        let optimizely = OptimizelyClient(sdkKey: sdkKey,
95
                                          eventProcessor: eventProcessor,
96
                                          eventDispatcher: eventDispatcher,
97
                                          userProfileService: userProfileService)
98

99
        do {
100
            guard let datafile = OTUtils.loadJSONDatafile(datafileName) else { return nil }
101
            try optimizely.start(datafile: datafile, doFetchDatafileBackground: false)
102

103
            return optimizely
104
        } catch {
105
            return nil
106
        }
107
    }
108
    
109
    // use legacy EventDispatcher
110
    static func createOptimizelyLegacy(sdkKey: String? = nil,
111
                                       datafileName: String,
112
                                       clearUserProfileService: Bool,
113
                                       eventDispatcher: OPTEventDispatcher) -> OptimizelyClient? {
114
        
115
        prepareDocumentFolderInSimulator()
116

117
        // use random sdkKey to avoid registration conflicts when multiple tests running in parallel
118
        let sdkKey = sdkKey ?? randomSdkKey
119

120
        //-------------------------------------------------------------------
121
        // reset previous services so that new EP (No SDKKey) can be registered OK
122
        //-------------------------------------------------------------------
123
        OTUtils.clearRegistryService()
124

125
        let userProfileService = clearUserProfileService ? createClearUserProfileService() : nil
126
        
127
        let optimizely = OptimizelyClient(sdkKey: sdkKey,
128
                                          eventDispatcher: eventDispatcher,
129
                                          userProfileService: userProfileService)
130
        do {
131
            guard let datafile = OTUtils.loadJSONDatafile(datafileName) else { return nil }
132
            try optimizely.start(datafile: datafile, doFetchDatafileBackground: false)
133
        
134
            return optimizely
135
        } catch {
136
            return nil
137
        }
138
    }
139
    
140
    // MARK: - big numbers
141
    
142
    static var positiveMaxValueAllowed: Double {
64✔
143
        return pow(2, 53)
64✔
144
    }
64✔
145
    
146
    static var negativeMaxValueAllowed: Double {
32✔
147
        return -pow(2, 53)
32✔
148
    }
32✔
149
    
150
    static var positiveTooBigValue: Double {
42✔
151
        return positiveMaxValueAllowed * 2.0
42✔
152
    }
42✔
153
    
154
    static var negativeTooBigValue: Double {
28✔
155
        return negativeMaxValueAllowed * 2.0
28✔
156
    }
28✔
157
    
158
    static func saveAFile(name:String, data:Data) -> URL? {
6✔
159
        if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
6✔
160
            
6✔
161
            let fileURL = dir.appendingPathComponent(name, isDirectory: false)
6✔
162
            
6✔
163
            try? data.write(to: fileURL, options: .atomic)
6✔
164
            return fileURL
6✔
165
        }
6✔
166
        
167
        return nil
168
    }
6✔
169
    
170
    // iOS11+ simulators do not have Document folder by default when launched.
171
    // Should be created manually.
172
    static func prepareDocumentFolderInSimulator() {
173
        if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
174
            if (!FileManager.default.fileExists(atPath: url.path)) {
175
                do {
176
                    try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil)
177
                } catch {
178
                    print(error)
179
                }
180
            }
181
        }
182
    }
183
    
184
    // MARK: - others
185
    
186
    static var randomSdkKey: String {
500✔
187
        return String(arc4random())
500✔
188
    }
500✔
189
    
190
    static let keyTestEventFileName = "EventProcessorTests-Batch---"
191
    static var uniqueEventFileName: String {
30✔
192
        return keyTestEventFileName + randomSdkKey
30✔
193
    }
30✔
194
    
195
    static func cleanupTestEventFiles() {
196
        // remove all event files used for testing
197
        
198
        let fm = FileManager.default
199
        let docFolder = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
200
        let allFiles = try! fm.contentsOfDirectory(atPath: docFolder)
201
        
202
        let predicate = NSPredicate(format: "self CONTAINS '\(OTUtils.keyTestEventFileName)'")
203
        let filtered = allFiles.filter { predicate.evaluate(with: $0) }
204
        
205
        filtered.forEach {
206
            do {
207
                try fm.removeItem(atPath: (docFolder as NSString).appendingPathComponent($0))
208
                print("[EventBatchTest] Removed temporary event file: \($0)")
209
            } catch {
210
                print("[EventBatchTest] ERROR: cannot remove temporary event file: \($0)")
211
            }
212
        }
213
    }
214
}
215

216
// MARK: - Test EventProcessor + EventDispatcher
217

218
class TestableBatchEventProcessor: BatchEventProcessor {
219
    let eventFileName: String
220
    
221
    init(eventDispatcher: OPTEventsDispatcher,
222
         eventFileName: String? = nil,
223
         removeDatafileObserver: Bool = true,
224
         disableBatch: Bool = false)
225
    {
226
        self.eventFileName = eventFileName ?? OTUtils.uniqueEventFileName
227
        
228
        if disableBatch {
229
            super.init(eventDispatcher: eventDispatcher,
230
                       batchSize: 1,
231
                       dataStoreName: self.eventFileName)
232
        } else {
233
            super.init(eventDispatcher: eventDispatcher,
234
                       dataStoreName: self.eventFileName)
235
        }
236
        
237
        print("[TestableEventProcessor] init with [\(self.eventFileName)] ")
238

239
        // block interference from other tests notifications when testing batch timing
240
        if removeDatafileObserver {
241
            removeProjectChangeNotificationObservers()
242
        }
243
    }
244
    
245
    override func process(event: UserEvent, completionHandler: DispatchCompletionHandler?) {
764✔
246
        super.process(event: event, completionHandler: completionHandler)
764✔
247
    }
764✔
248
}
249

250
class TestableHTTPEventDispatcher: HTTPEventDispatcher {
251
    var sendRequestedEvents: [EventForDispatch] = []
252
    var forceError = false
253
    var numReceivedVisitors = 0
254

255
    // set this if need to wait sendEvent completed
256
    var exp: XCTestExpectation?
257
    
258
    override func dispatch(event: EventForDispatch, completionHandler: DispatchCompletionHandler?) {
149✔
259
        sendRequestedEvents.append(event)
149✔
260
        
149✔
261
        do {
149✔
262
            let decodedEvent = try JSONDecoder().decode(BatchEvent.self, from: event.body)
149✔
263
            numReceivedVisitors += decodedEvent.visitors.count
149✔
264
            print("[TestableEventProcessor][SendEvent] Received a batched event with visitors: \(decodedEvent.visitors.count) \(numReceivedVisitors)")
149✔
265
        } catch {
149✔
266
            // invalid event format detected
×
267
            // - invalid events are supposed to be filtered out when batching (converting to nil, so silently dropped)
×
268
            // - an exeption is that an invalid event is alone in the queue, when validation is skipped for performance on common path
×
269
            
×
270
            // pass through invalid events, so server can filter them out
×
271
        }
149✔
272

149✔
273
        // must call completionHandler to complete synchronization
149✔
274
        super.dispatch(event: event) { _ in
149✔
275
            if self.forceError {
149✔
276
                completionHandler?(.failure(.eventDispatchFailed("forced")))
16✔
277
            } else {
149✔
278
                // return success to clear store after sending events
133✔
279
                completionHandler?(.success(Data()))
133✔
280
            }
149✔
281

149✔
282
            self.exp?.fulfill()
149✔
283
        }
149✔
284
    }
149✔
285
    
286
    func clear() {
287
        super.clear()
288
        sendRequestedEvents = []
289
        numReceivedVisitors = 0
290
    }
291
}
292

293
// MARK: - Test DefaultEventDispatcher
294

295
class TestableDefaultEventDispatcher: DefaultEventDispatcher {
296
    var sendRequestedEvents: [EventForDispatch] = []
297
    var forceError = false
298
    var numReceivedVisitors = 0
299
    let eventFileName: String
300
    
301
    // set this if need to wait sendEvent completed
302
    var exp: XCTestExpectation?
303
    
304
    init(eventFileName: String? = nil,
305
         removeDatafileObserver: Bool = true,
306
         disableBatch: Bool = false) {
307
        
308
        self.eventFileName = eventFileName ?? OTUtils.uniqueEventFileName
309
        if disableBatch {
310
            super.init(batchSize: 1, dataStoreName: self.eventFileName)
311
        } else {
312
            super.init(dataStoreName: self.eventFileName)
313
        }
314
        
315
        // block interference from other tests notifications when testing batch timing
316
        if removeDatafileObserver {
317
            removeProjectChangeNotificationObservers()
318
        }
319
    }
320
    
321
    override func sendEvent(event: EventForDispatch, completionHandler: @escaping DispatchCompletionHandler) {
308✔
322
        sendRequestedEvents.append(event)
308✔
323
        
308✔
324
        do {
308✔
325
            let decodedEvent = try JSONDecoder().decode(BatchEvent.self, from: event.body)
308✔
326
            numReceivedVisitors += decodedEvent.visitors.count
308✔
327
            print("[TestableEventDispatcher][SendEvent][\(self.eventFileName)] Received a batched event with visitors: \(decodedEvent.visitors.count) \(numReceivedVisitors)")
308✔
328
        } catch {
308✔
329
            // invalid event format detected
4✔
330
            // - invalid events are supposed to be filtered out when batching (converting to nil, so silently dropped)
4✔
331
            // - an exeption is that an invalid event is alone in the queue, when validation is skipped for performance on common path
4✔
332
            
4✔
333
            // pass through invalid events, so server can filter them out
4✔
334
        }
308✔
335

308✔
336
        // must call completionHandler to complete synchronization
308✔
337
        super.sendEvent(event: event) { _ in
308✔
338
            if self.forceError {
308✔
339
                completionHandler(.failure(.eventDispatchFailed("forced")))
16✔
340
            } else {
308✔
341
                // return success to clear store after sending events
292✔
342
                completionHandler(.success(Data()))
292✔
343
            }
308✔
344

308✔
345
            self.exp?.fulfill()
308✔
346
        }
308✔
347
    }
308✔
348
}
349

350
// MARK: - Mock EventProcessor + EventDispatcher
351

352
class MockEventProcessor: OPTEventsProcessor {
353
    public var events = [UserEvent]()
354
    required init() {}
355
    
356
    func process(event: UserEvent, completionHandler: DispatchCompletionHandler?) {
×
357
        events.append(event)
×
358
        completionHandler?(.success(Data()))
×
359
    }
×
360
    
361
    func flush() {
362
        events.removeAll()
363
    }
364
}
365

366
class MockEventDispatcher: OPTEventsDispatcher {
367
    public var events = [EventForDispatch]()
368
    required init() {}
176✔
369

370
    func dispatch(event: EventForDispatch, completionHandler: DispatchCompletionHandler?) {
80✔
371
        events.append(event)
80✔
372
        completionHandler?(.success(Data()))
80✔
373
    }
80✔
374
    
375
    func clear() {
376
        events = []
377
    }
378
}
379

380
// MARK: - Mock Legacy EventDispatcher
381

382
class MockLagacyEventDispatcher: OPTEventDispatcher {
383
    public var events = [EventForDispatch]()
384
    required init() {}
60✔
385
    
386
    func dispatchEvent(event: EventForDispatch, completionHandler: DispatchCompletionHandler?) {
52✔
387
        events.append(event)
52✔
388
        completionHandler?(.success(Data()))
52✔
389
    }
52✔
390
    
391
    /// Attempts to flush the event queue if there are any events to process.
392
    func flushEvents() {
393
        events.removeAll()
394
    }
395
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2024 Coveralls, Inc