diff --git a/Sources/CXExtensions/AnyScheduler.swift b/Sources/CXExtensions/AnyScheduler.swift new file mode 100644 index 0000000..bee83fc --- /dev/null +++ b/Sources/CXExtensions/AnyScheduler.swift @@ -0,0 +1,289 @@ +import CXShim + +private enum SchedulerTimeLiteral { + + case seconds(Int) + case milliseconds(Int) + case microseconds(Int) + case nanoseconds(Int) + case interval(Double) + + var timeInterval: Double { + switch self { + case let .seconds(s): return Double(s) + case let .milliseconds(ms): return Double(ms) * 1_000 + case let .microseconds(us): return Double(us) * 1_000_000 + case let .nanoseconds(ns): return Double(ns) * 1_000_000_000 + case let .interval(s): return s + } + } +} + +/// A type-erasing scheduler. +/// +/// Do not use `SchedulerTimeType` across different `AnyScheduler` instance. +/// +/// let scheduler1 = AnyScheduler(DispatchQueue.main.cx) +/// let scheduler2 = AnyScheduler(RunLoop.main.cx) +/// +/// let time1 = scheduler1.now +/// let time2 = scheduler2.now +/// +/// // DON'T DO THIS! +/// time1.distance(to: time2) // Will crash. +/// +public final class AnyScheduler: Scheduler { + + public typealias SchedulerOptions = Never + public typealias SchedulerTimeType = AnySchedulerTimeType + + private let _now: () -> SchedulerTimeType + private let _minimumTolerance: () -> SchedulerTimeType.Stride + private let _schedule_action: (@escaping () -> Void) -> Void + private let _schedule_after_tolerance_action: (SchedulerTimeType, SchedulerTimeType.Stride, @escaping () -> Void) -> Void + private let _schedule_after_interval_tolerance_action: (SchedulerTimeType, SchedulerTimeType.Stride, SchedulerTimeType.Stride, @escaping () -> Void) -> Cancellable + + public init(_ scheduler: S, options: S.SchedulerOptions? = nil) { + _now = { + SchedulerTimeType(wrapping: scheduler.now) + } + _minimumTolerance = { + SchedulerTimeType.Stride(wrapping: scheduler.minimumTolerance) + } + _schedule_action = { action in + scheduler.schedule(options: options, action) + } + _schedule_after_tolerance_action = { date, tolerance, action in + scheduler.schedule(after: date.wrapped as! S.SchedulerTimeType, tolerance: tolerance.asType(S.SchedulerTimeType.Stride.self), options: options, action) + } + _schedule_after_interval_tolerance_action = { date, interval, tolerance, action in + scheduler.schedule(after: date.wrapped as! S.SchedulerTimeType, interval: interval.asType(S.SchedulerTimeType.Stride.self), tolerance: tolerance.asType(S.SchedulerTimeType.Stride.self), options: options, action) + } + } + + public var now: SchedulerTimeType { + return _now() + } + + public var minimumTolerance: SchedulerTimeType.Stride { + return _minimumTolerance() + } + + public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) { + return _schedule_action(action) + } + + public func schedule(after date: SchedulerTimeType, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) { + return _schedule_after_tolerance_action(date, tolerance, action) + } + + public func schedule(after date: SchedulerTimeType, interval: SchedulerTimeType.Stride, tolerance: SchedulerTimeType.Stride, options: SchedulerOptions?, _ action: @escaping () -> Void) -> Cancellable { + return _schedule_after_interval_tolerance_action(date, interval, tolerance, action) + } +} + +public struct AnySchedulerTimeType: Strideable { + + public struct Stride: Comparable, SignedNumeric, SchedulerTimeIntervalConvertible { + + private struct Opaque { + + let wrapped: Any + + let _init: (SchedulerTimeLiteral) -> Opaque + let _lessThan: (Any) -> Bool + let _equalTo: (Any) -> Bool + let _add: (Any) -> Opaque + let _subtract: (Any) -> Opaque + let _multiply: (Any) -> Opaque + + init(_ content: T) { + wrapped = content + _init = { Opaque(T.time(literal: $0)) } + _lessThan = { content < ($0 as! T) } + _equalTo = { content < ($0 as! T) } + _add = { Opaque(content + ($0 as! T)) } + _subtract = { Opaque(content - ($0 as! T)) } + _multiply = { Opaque(content * ($0 as! T)) } + } + } + + private enum Wrapped { + case opaque(Opaque) + case literal(SchedulerTimeLiteral) + } + + private var wrapped: Wrapped + + private init(_ value: Wrapped) { + wrapped = value + } + + fileprivate init(wrapping opaque: T) { + wrapped = .opaque(.init(opaque)) + } + + fileprivate func asType(_ type: T.Type) -> T { + switch wrapped { + case let .opaque(opaque): + return opaque.wrapped as! T + case let .literal(literal): + return T.time(literal: literal) + } + } + + public init(integerLiteral value: Int) { + wrapped = .literal(.seconds(value)) + } + + public init?(exactly source: T) { + guard let value = Int(exactly: source) else { + return nil + } + self.init(integerLiteral: value) + } + + public var magnitude: Stride { + // TODO: magnitude? + fatalError() + } + + public static func == (lhs: Stride, rhs: Stride) -> Bool { + switch (lhs.wrapped, rhs.wrapped) { + case let (.opaque(l), .opaque(r)): + return l._equalTo(r.wrapped) + case let (.opaque(l), .literal(r)): + return l._equalTo(l._init(r).wrapped) + case let (.literal(l), .opaque(r)): + return r._init(l)._equalTo(r.wrapped) + case let (.literal(l), .literal(r)): + // TODO: potential precision loss + return l.timeInterval == r.timeInterval + } + } + + public static func < (lhs: Stride, rhs: Stride) -> Bool { + switch (lhs.wrapped, rhs.wrapped) { + case let (.opaque(l), .opaque(r)): + return l._lessThan(r.wrapped) + case let (.opaque(l), .literal(r)): + return l._lessThan(l._init(r).wrapped) + case let (.literal(l), .opaque(r)): + return r._init(l)._lessThan(r.wrapped) + case let (.literal(l), .literal(r)): + // TODO: potential precision loss + return l.timeInterval < r.timeInterval + } + } + + public static func + (lhs: Stride, rhs: Stride) -> Stride { + switch (lhs.wrapped, rhs.wrapped) { + case let (.opaque(l), .opaque(r)): + return .init(.opaque(l._add(r.wrapped))) + case let (.opaque(l), .literal(r)): + return .init(.opaque(l._add(l._init(r).wrapped))) + case let (.literal(l), .opaque(r)): + return .init(.opaque(r._init(l)._add(r.wrapped))) + case let (.literal(l), .literal(r)): + // TODO: potential precision loss + return .seconds(l.timeInterval + r.timeInterval) + } + } + + public static func - (lhs: Stride, rhs: Stride) -> Stride { + switch (lhs.wrapped, rhs.wrapped) { + case let (.opaque(l), .opaque(r)): + return .init(.opaque(l._subtract(r.wrapped))) + case let (.opaque(l), .literal(r)): + return .init(.opaque(l._subtract(l._init(r).wrapped))) + case let (.literal(l), .opaque(r)): + return .init(.opaque(r._init(l)._subtract(r.wrapped))) + case let (.literal(l), .literal(r)): + // TODO: potential precision loss + return .seconds(l.timeInterval - r.timeInterval) + } + } + + public static func * (lhs: Stride, rhs: Stride) -> Stride { + switch (lhs.wrapped, rhs.wrapped) { + case let (.opaque(l), .opaque(r)): + return .init(.opaque(l._multiply(r.wrapped))) + case let (.opaque(l), .literal(r)): + return .init(.opaque(l._multiply(l._init(r).wrapped))) + case let (.literal(l), .opaque(r)): + return .init(.opaque(r._init(l)._multiply(r.wrapped))) + case let (.literal(l), .literal(r)): + // TODO: potential precision loss + return .seconds(l.timeInterval * r.timeInterval) + } + } + + public static func += (lhs: inout Stride, rhs: Stride) { + lhs = lhs + rhs + } + + public static func -= (lhs: inout Stride, rhs: Stride) { + lhs = lhs - rhs + } + + public static func *= (lhs: inout Stride, rhs: Stride) { + lhs = lhs * rhs + } + + public static func seconds(_ s: Double) -> Stride { + return Stride(.literal(.interval(s))) + } + + public static func seconds(_ s: Int) -> Stride { + return Stride(.literal(.seconds(s))) + } + + public static func milliseconds(_ ms: Int) -> Stride { + return Stride(.literal(.milliseconds(ms))) + } + + public static func microseconds(_ us: Int) -> Stride { + return Stride(.literal(.microseconds(us))) + } + + public static func nanoseconds(_ ns: Int) -> Stride { + return Stride(.literal(.nanoseconds(ns))) + } + } + + fileprivate let wrapped: Any + + private let _distance_to: (Any) -> Stride + private let _advanced_by: (Stride) -> AnySchedulerTimeType + + fileprivate init(wrapping opaque: T) where T.Stride: SchedulerTimeIntervalConvertible { + self.wrapped = opaque + self._distance_to = { other in + return Stride(wrapping: opaque.distance(to: other as! T)) + } + self._advanced_by = { n in + return AnySchedulerTimeType(wrapping: opaque.advanced(by: n.asType(T.Stride.self))) + } + } + + public func distance(to other: AnySchedulerTimeType) -> Stride { + return _distance_to(other) + } + + public func advanced(by n: Stride) -> AnySchedulerTimeType { + return _advanced_by(n) + } +} + +private extension SchedulerTimeIntervalConvertible { + + static func time(literal: SchedulerTimeLiteral) -> Self { + switch literal { + case let .seconds(s): return .seconds(s) + case let .milliseconds(ms): return .milliseconds(ms) + case let .microseconds(us): return .microseconds(us) + case let .nanoseconds(ns): return .nanoseconds(ns) + case let .interval(s): return .seconds(s) + } + } +} diff --git a/Tests/CXExtensionsTests/AnySchedulerSpec.swift b/Tests/CXExtensionsTests/AnySchedulerSpec.swift new file mode 100644 index 0000000..589fe2a --- /dev/null +++ b/Tests/CXExtensionsTests/AnySchedulerSpec.swift @@ -0,0 +1,39 @@ +import Quick +import Nimble +import CXTest +import CXShim +import CXExtensions + +final class AnySchedulerSpec: QuickSpec { + + override func spec() { + + it("should wrap Scheduler") { + let scheduler = VirtualTimeScheduler() + let anyScheduler = AnyScheduler(scheduler) + var events: [Int] = [] + var cancellers = Set() + anyScheduler.schedule { + events.append(1) + } + anyScheduler.schedule(after: anyScheduler.now.advanced(by: .seconds(10))) { + events.append(2) + anyScheduler.schedule(after: anyScheduler.now.advanced(by: .seconds(20))) { + events.append(3) + cancellers.removeAll() + } + } + anyScheduler.schedule { + events.append(4) + } + anyScheduler.schedule(after: anyScheduler.now.advanced(by: .seconds(5)), interval: .seconds(10)) { + events.append(5) + }.store(in: &cancellers) + scheduler.advance(by: 0) + expect(events) == [1, 4] + scheduler.advance(by: 40) + expect(events) == [1, 4, 5, 2, 5, 5, 3] + // time: 0, 0, 5, 10,15,25,30 + } + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 3848611..b88cfff 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -4,6 +4,7 @@ import XCTest @testable import CXExtensionsTests QCKMain([ + AnySchedulerSpec.self, BlockingSpec.self, DelayedAutoCancellableSpec.self, IgnoreErrorSpec.self,