// // TestScheduler+MarbleTests.swift // RxExample // // Created by Krunoslav Zaher on 12/29/15. // Copyright © 2015 Krunoslav Zaher. All rights reserved. // import Foundation import RxSwift import RxTests import RxCocoa /** There are examples like this all over the web, but I think that I've first something like this here https://github.com/ReactiveX/RxJS/blob/master/doc/writing-marble-tests.md These tests are called marble tests. */ extension TestScheduler { /** Transformation from this format: ---a---b------c----- to this format schedule onNext(1) @ 0.6s schedule onNext(2) @ 1.4s schedule onNext(3) @ 7.0s .... ] You can also specify retry data in this format: ---a---b------c----#|----a--#|----b - letters and digits mark values - `#` marks unknown error - `|` marks sequence completed */ func parseEventsAndTimes(timeline: String, values: [String: T], errors: [String: Swift.Error] = [:]) -> [[Recorded>]] { //print("parsing: \(timeline)") typealias RecordedEvent = Recorded> let timelines = timeline.components(separatedBy: "|") let allExceptLast = timelines[0 ..< timelines.count - 1] return (allExceptLast.map { $0 + "|" } + [timelines.last!]) .filter { $0.characters.count > 0 } .map { timeline -> [Recorded>] in let segments = timeline.components(separatedBy:"-") let (time: _, events: events) = segments.reduce((time: 0, events: [RecordedEvent]())) { state, event in let tickIncrement = event.characters.count + 1 if event.characters.count == 0 { return (state.time + tickIncrement, state.events) } if event == "#" { let errorEvent = RecordedEvent(time: state.time, event: Event.error(NSError(domain: "Any error domain", code: -1, userInfo: nil))) return (state.time + tickIncrement, state.events + [errorEvent]) } if event == "|" { let completed = RecordedEvent(time: state.time, event: Event.completed) return (state.time + tickIncrement, state.events + [completed]) } guard let next = values[event] else { guard let error = errors[event] else { fatalError("Value with key \(event) not registered as value:\n\(values)\nor error:\n\(errors)") } let nextEvent = RecordedEvent(time: state.time, event: Event.error(error)) return (state.time + tickIncrement, state.events + [nextEvent]) } let nextEvent = RecordedEvent(time: state.time, event: Event.next(next)) return (state.time + tickIncrement, state.events + [nextEvent]) } //print("parsed: \(events)") return events } } /** Creates driver for marble test. - parameter timeline: Timeline in the form `---a---b------c--|` - parameter values: Dictionary of values in timeline. `[a:1, b:2]` - returns: Driver specified by timeline and values. */ func createDriver(timeline: String, values: [String: T]) -> Driver { return createObservable(timeline: timeline, values: values, errors: [:]).asDriver(onErrorRecover: { (error) -> Driver in fatalError("This can't error out") return Driver.never() }) } /** Creates observable for marble tests. - parameter timeline: Timeline in the form `---a---b------c--|` - parameter values: Dictionary of values in timeline. `[a:1, b:2]` - parameter errors: Dictionary of errors in timeline. - returns: Observable sequence specified by timeline and values. */ func createObservable(timeline: String, values: [String: T], errors: [String: Swift.Error] = [:]) -> Observable { let events = self.parseEventsAndTimes(timeline: timeline, values: values, errors: errors) return createObservable(events) } /** Creates observable for marble tests. - parameter events: Recorded events to replay. - returns: Observable sequence specified by timeline and values. */ func createObservable(_ events: [Recorded>]) -> Observable { return createObservable([events]) } /** Creates observable for marble tests. - parameter events: Recorded events to replay. This overloads enables modeling of retries. `---a---b------c----#|----a--#|----b` When next observer is subscribed, next sequence will be replayed. If all sequences have been replayed and new observer is subscribed, `fatalError` will be raised. - returns: Observable sequence specified by timeline and values. */ func createObservable(_ events: [[Recorded>]]) -> Observable { var attemptCount = 0 print("created for \(events)") return Observable.create { observer in if attemptCount >= events.count { fatalError("This is attempt # \(attemptCount + 1), but timeline only allows \(events.count).\n\(events)") } let scheduledEvents = events[attemptCount].map { event in return self.scheduleRelative((), dueTime: resolution * TimeInterval(event.time)) { _ in observer.on(event.value) return Disposables.create() } } attemptCount += 1 return Disposables.create(scheduledEvents) } } /** Enables simple construction of mock implementations from marble timelines. - parameter Arg: Type of arguments of mocked method. - parameter Ret: Return type of mocked method. `Observable` - parameter values: Dictionary of values in timeline. `[a:1, b:2]` - parameter errors: Dictionary of errors in timeline. - parameter timelineSelector: Method implementation. The returned string value represents timeline of returned observable sequence. `---a---b------c----#|----a--#|----b` - returns: Implementation of method that accepts arguments with parameter `Arg` and returns observable sequence with parameter `Ret`. */ func mock(values: [String: Ret], errors: [String: Swift.Error] = [:], timelineSelector: @escaping (Arg) -> String) -> (Arg) -> Observable { return { (parameters: Arg) -> Observable in let timeline = timelineSelector(parameters) return self.createObservable(timeline: timeline, values: values, errors: errors) } } /** Builds testable observer for s specific observable sequence, binds it's results and sets up disposal. - parameter source: Observable sequence to observe. - returns: Observer that records all events for observable sequence. */ func record(source: O) -> TestableObserver { let observer = self.createObserver(O.E.self) let disposable = source.asObservable().bindTo(observer) self.scheduleAt(100000) { disposable.dispose() } return observer } }