Skip to content

Instantly share code, notes, and snippets.

@danielt1263
Last active November 13, 2022 12:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielt1263/bd449100764e3166644f7a38bca86c96 to your computer and use it in GitHub Desktop.
Save danielt1263/bd449100764e3166644f7a38bca86c96 to your computer and use it in GitHub Desktop.
//
// TestScheduler+MarbleTests.swift
//
// Created by Daniel Tartaglia on 31 October 2021.
// Copyright © 2022 Daniel Tartaglia. MIT License.
//
import Foundation
import RxSwift
import RxTest
extension TestScheduler {
/**
Creates an Observable that emits the elements in timeline up to the first stop event (or end of the
timeline if no stop events exist). If the Observable is subscribed to a second time, it will emit the next set
of events, up to the next stop event (or the end), in the timeline, or loop back to the first set of events if
it just handled the last set.
- Parameter timeline: A string representing the marble diagrams that this observable will emit.
- Parameter errors: A dictionary defining any substrings in the timeline that represent custom error
objects. Defaults to empty which means only the `#` can be used to emit an error.
- Parameter resolution: A closure telling the function what resolution to use when timing
events. Defaults to one second per character.
- Returns: An Observable that behaves as defined above.
*/
func createObservable(
timeline: String,
errors: [String.Element: Error] = [:]
) -> Observable<String> {
let events = parseEventsAndTimes(timeline: timeline, values: { String($0) }, errors: { errors[$0] })
return createObservable(events)
}
/**
Creates an Observable that emits the elements in timeline up to the first stop event (or end of the
timeline if no stop events exist). If the Observable is subscribed to a second time, it will emit the next set
of events, up to the next stop event (or the end), in the timeline, or loop back to the first set of events if
it just handled the last set.
- Parameter timeline: A string representing the marble diagrams that this observable will emit.
- Parameter values: A dictionary defining any substrings in the timeline that represent a next
element.
- Parameter errors: A dictionary defining any substrings in the timeline that represent custom error
objects. Defaults to empty which means only the `#` can be used to emit a generic error.
- Parameter resolution: A closure telling the function what resolution to use when timing
events. Defaults to one second per character.
- Returns: An Observable that behaves as defined above.
*/
func createObservable<T>(
timeline: String,
values: [String.Element: T],
errors: [String.Element: Error] = [:]
) -> Observable<T> {
let events = parseEventsAndTimes(timeline: timeline, values: values, errors: errors)
return createObservable(events)
}
/**
Creates an Observable that emits the recorded events in the provided array. If the Observable is
subscribed to a second time, it will replay the array of events.
- Parameter events: An array of recorded events to play.
- Parameter resolution: A closure telling the function what resolution to use when timing
events. Defaults to one second times the `Recorded` event's test time.
- Returns: An Observable that behaves as defined above.
*/
func createObservable<T>(
_ events: [Recorded<Event<T>>]
) -> Observable<T> {
createObservable([events])
}
/**
Creates an Observable that emits the recorded events in the first array of the provided jagged array. Each time
the Observable is subscribed to, it will emit the recorded events in the next array, or loop back to the first array
if it just handled the last array.
- Parameter events: A jagged array of recorded events to play.
- Parameter resolution: A closure telling the function what resolution to use when timing
events. Defaults to one second times the `Recorded` event's test time.
- Returns: An Observable that behaves as defined above.
*/
func createObservable<T>(
_ events: [[Recorded<Event<T>>]]
) -> Observable<T> {
var attemptCount = 0
return Observable.deferred {
defer { attemptCount += 1 }
return self.createColdObservable(events[attemptCount % events.count])
.asObservable()
}
}
/**
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<Ret>`
- parameter args: parameters passed into mock.
- 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<Arg, Ret>(
args: TestableObserver<Arg>,
values: [String.Element: Ret],
errors: [String.Element: Error] = [:],
timelineSelector: @escaping (Arg) -> String
) -> (Arg) -> Observable<Ret> {
return { (parameters: Arg) -> Observable<Ret> in
args.onNext(parameters)
let timeline = timelineSelector(parameters)
return self.createObservable(timeline: timeline, values: values, errors: errors)
}
}
/**
Enables simple construction of mock implementations from marble timelines.
- parameter Arg: Type of arguments of mocked method.
- parameter args: parameters passed into mock.
- 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
of Strings.
*/
func mock<Arg>(
args: TestableObserver<Arg>,
errors: [String.Element: Error] = [:],
timelineSelector: @escaping (Arg) -> String
) -> (Arg) -> Observable<String> {
return { (parameters: Arg) -> Observable<String> in
args.onNext(parameters)
let timeline = timelineSelector(parameters)
let events = parseEventsAndTimes(timeline: timeline, values: { String($0) }, errors: { errors[$0] })
return self.createObservable(events)
}
}
}
/**
Creates events arrays based on an input marble diagram. Timelines can continue after a stop event which
will begin a new array of events.
Special characters in the timeline:
"|": represents a completed stream.
"#": represents a generic error in a stream.
"-": represents the advancing of time one time-unit without emitting a value.
Any other character represents a value of type T, or a specialized Error type. The function will first query the
`errors` closure to lookup the character, if not found there it will call the `values` closure. If neither
closure returns an object for the substring, then an error will be asserted.
- Parameter timeline: A string that follows the rules as defined above.
- parameter values: Dictionary of values in timeline. `[a:1, b:2]`
- parameter errors: Dictionary of errors in timeline. Defaults to empty which means only the `#` can be used to emit a generic error.
- Returns: An array of event arrays.
*/
func parseEventsAndTimes<T>(
timeline: String,
values: [String.Element: T],
errors: [String.Element: Error] = [:],
defaultError: Error = NSError(domain: "Test Domain", code: -1, userInfo: nil)
) -> [[Recorded<Event<T>>]] {
parseEventsAndTimes(
timeline: timeline,
values: { values[$0].map { [$0] } ?? [] },
errors: { errors[$0] },
defaultError: defaultError
)
}
/**
Creates events arrays based on an input marble diagram. Timelines can continue after a stop event which will begin a
new array of events.
Special characters in the timeline:
"|": represents a completed stream.
"#": represents a generic error in a stream.
"-": represents the advancing of time one time-unit without emitting a value.
Any other character represents a value of type T, or a specialized Error type. The function will first query the
`errors` closure to lookup the character, if not found there it will call the `values` closure. If neither
closure returns an object for the substring, then an error will be asserted.
- Parameter timeline: A string that follows the rules as defined above.
- parameter values: Dictionary of values in timeline. `[a:[1], b:[2]]`. Allows for two values to be emitted at the
same time-point.
- parameter errors: Dictionary of errors in timeline. Defaults to empty which means only the `#` can be used to emit a
generic error.
- Returns: An array of event arrays.
*/
func parseEventsAndTimes<T>(
timeline: String,
values: [String.Element: [T]],
errors: [String.Element: Error] = [:],
defaultError: Error = NSError(domain: "Test Domain", code: -1, userInfo: nil)
) -> [[Recorded<Event<T>>]] {
parseEventsAndTimes(
timeline: timeline,
values: { values[$0] ?? [] },
errors: { errors[$0] },
defaultError: defaultError
)
}
/**
Creates events arrays based on an input marble diagram. Timelines can continue after a stop event which
will begin a new array of events.
Special characters in the timeline:
"|": represents a completed stream.
"#": represents a generic error in a stream.
"-": represents the advancing of time one time-unit without emitting a value.
Any other character represents a value of type T, or a specialized Error type. The function will first query the
`errors` closure to lookup the character, if not found there it will call the `values` closure. If neither
closure returns an object for the substring, then an error will be asserted.
- Parameter timeline: A string that follows the rules as defined above.
- Parameter values: A closure defining any substrings in the timeline that represent a next element.
- Parameter errors: A closure defining any substrings in the timeline that represent custom error
objects. Defaults to none which means only the `#` can be used to emit a generic error.
- Returns: An array of event arrays.
*/
func parseEventsAndTimes<T>(
timeline: String,
values: (String.Element) -> T?,
errors: (String.Element) -> Error? = { _ in nil },
defaultError: Error = NSError(domain: "Test Domain", code: -1, userInfo: nil)
) -> [[Recorded<Event<T>>]] {
parseEventsAndTimes(
timeline: timeline,
values: { values($0).map { [$0] } ?? [] },
errors: errors,
defaultError: defaultError
)
}
/**
Creates events arrays based on an input marble diagram. Timelines can continue after a stop event which will begin a
new array of events.
Special characters in the timeline:
"|": represents a completed stream.
"#": represents a generic error in a stream.
"-": represents the advancing of time one time-unit without emitting a value.
Any other character represents a value of type T, or a specialized Error type. The function will first query the
`errors` closure to lookup the character, if not found there it will call the `values` closure. If the `values`
closure returns an empty array, it will be treated as "-".
- Parameter timeline: A string that follows the rules as defined above.
- Parameter values: A closure defining any substrings in the timeline that represent a next element. This returns an
array incase there is more than one next event at the same time-point.
- Parameter errors: A closure defining any substrings in the timeline that represent custom error objects. Defaults to
none which means only the `#` can be used to emit a generic error.
- Returns: An array of event arrays.
*/
func parseEventsAndTimes<T>(
timeline: String,
values: (String.Element) -> [T],
errors: (String.Element) -> Error? = { _ in nil },
defaultError: Error = NSError(domain: "Test Domain", code: -1, userInfo: nil)
) -> [[Recorded<Event<T>>]] {
return timeline.reduce(into: EventsAndTimeState<T>()) { state, char in
if state.tick == 0 {
state.output.append([])
}
let index = state.output.count - 1
switch char {
case "|":
state.output[index].append(.completed(max(state.tick - 1, 0)))
state.tick = 0
case "#":
state.output[index].append(.error(state.tick, defaultError))
state.tick = 0
case "-":
state.tick += 1
default:
if let error = errors(char) {
state.output[index].append(.error(state.tick, error))
state.tick = 0
} else {
for element in values(char) {
state.output[index].append(.next(state.tick, element))
}
state.tick += 1
}
}
}
.output
}
extension Array {
func offsetTime<T>(by time: Int) -> [[Recorded<Event<T>>]] where Element == [Recorded<Event<T>>] {
map { $0.map { Recorded(time: $0.time + time, value: $0.value) } }
}
}
private struct EventsAndTimeState<T> {
var output: [[Recorded<Event<T>>]] = []
var tick: Int = 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment