Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added inaccurate test schedulers #72

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions Sources/CombineSchedulers/TestScheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,18 @@

private var lastSequence: UInt = 0
private let lock = NSRecursiveLock()
public let minimumTolerance: SchedulerTimeType.Stride = .zero
public let minimumTolerance: SchedulerTimeType.Stride
public private(set) var now: SchedulerTimeType
private var scheduled: [(sequence: UInt, date: SchedulerTimeType, action: () -> Void)] = []

/// Creates a test scheduler with the given date.
///
/// - Parameter now: The current date of the test scheduler.
public init(now: SchedulerTimeType) {
/// - Parameter minimumTolerance: An additional delay which will be added to the `.now`
/// property before executing units of work.
public init(now: SchedulerTimeType, minimumTolerance: SchedulerTimeType.Stride = .zero) {
self.now = now
self.minimumTolerance = minimumTolerance
}

/// Advances the scheduler by the given stride.
Expand All @@ -101,15 +104,15 @@
public func advance(by duration: SchedulerTimeType.Stride = .zero) async {
await self.advance(to: self.now.advanced(by: duration))
}

/// Advances the scheduler to the given instant.
///
/// - Parameter instant: An instant in time to advance to.
public func advance(to instant: SchedulerTimeType) {
while self.lock.sync(operation: { self.now }) <= instant {
self.lock.lock()
self.scheduled.sort { ($0.date, $0.sequence) < ($1.date, $1.sequence) }

guard
let next = self.scheduled.first,
instant >= next.date
Expand All @@ -118,7 +121,7 @@
self.lock.unlock()
return
}

self.now = next.date
self.scheduled.removeFirst()
self.lock.unlock()
Expand All @@ -145,7 +148,7 @@
self.lock.unlock()
return true
}

self.now = next.date
self.scheduled.removeFirst()
self.lock.unlock()
Expand Down Expand Up @@ -211,18 +214,27 @@
_ action: @escaping () -> Void
) -> Cancellable {
let sequence = self.lock.sync { self.nextSequence() }

let minimumTolerance = self.minimumTolerance

func scheduleAction(for date: SchedulerTimeType) -> () -> Void {
return { [weak self] in
let nextDate = date.advanced(by: interval)
self?.lock.sync {
self?.scheduled.append((sequence, nextDate, scheduleAction(for: nextDate)))
self?.scheduled.append((
sequence,
nextDate.advanced(by: minimumTolerance),
scheduleAction(for: nextDate)
))
}
action()
}
}

self.lock.sync { self.scheduled.append((sequence, date, scheduleAction(for: date))) }
self.lock.sync { self.scheduled.append((
sequence,
date.advanced(by: minimumTolerance),
scheduleAction(for: date)
)) }

return AnyCancellable { [weak self] in
self?.lock.sync { self?.scheduled.removeAll(where: { $0.sequence == sequence }) }
Expand All @@ -235,7 +247,11 @@
options _: SchedulerOptions?,
_ action: @escaping () -> Void
) {
self.lock.sync { self.scheduled.append((self.nextSequence(), date, action)) }
self.lock.sync { self.scheduled.append((
self.nextSequence(),
date.advanced(by: minimumTolerance),
action
)) }
}

public func schedule(options _: SchedulerOptions?, _ action: @escaping () -> Void) {
Expand All @@ -254,6 +270,13 @@
// NB: `DispatchTime(uptimeNanoseconds: 0) == .now())`. Use `1` for consistency.
.init(now: .init(.init(uptimeNanoseconds: 1)))
}

/// A test scheduler of dispatch queues that simulates the inaccuracy of real schedulers.
public static func inaccurate(
by minimumTolerance: SchedulerTimeType.Stride
) -> TestSchedulerOf<DispatchQueue> {
.init(now: .init(.init(uptimeNanoseconds: 1)), minimumTolerance: minimumTolerance)
}
}

extension UIScheduler {
Expand All @@ -262,20 +285,45 @@
// NB: `DispatchTime(uptimeNanoseconds: 0) == .now())`. Use `1` for consistency.
.init(now: .init(.init(uptimeNanoseconds: 1)))
}

/// A test scheduler compatible with type erased UI schedulers that simulates the inaccuracy of
/// real schedulers.
@_disfavoredOverload
public static func test(
minimumTolerance: SchedulerTimeType.Stride
) -> TestSchedulerOf<UIScheduler> {
.init(now: .init(.init(uptimeNanoseconds: 1)), minimumTolerance: minimumTolerance)
}
}

extension OperationQueue {
/// A test scheduler of operation queues.
public static var test: TestSchedulerOf<OperationQueue> {
.init(now: .init(.init(timeIntervalSince1970: 0)))
}

/// A test scheduler of operations queues that simulates the inaccuracy of real schedulers.
@_disfavoredOverload
public static func test(
minimumTolerance: SchedulerTimeType.Stride
) -> TestSchedulerOf<OperationQueue> {
.init(now: .init(.init(timeIntervalSince1970: 0)), minimumTolerance: minimumTolerance)
}
}

extension RunLoop {
/// A test scheduler of run loops.
public static var test: TestSchedulerOf<RunLoop> {
.init(now: .init(.init(timeIntervalSince1970: 0)))
}

/// A test scheduler of run loops that simulates the inaccuracy of real schedulers.
@_disfavoredOverload
public static func test(
minimumTolerance: SchedulerTimeType.Stride
) -> TestSchedulerOf<RunLoop> {
.init(now: .init(.init(timeIntervalSince1970: 0)), minimumTolerance: minimumTolerance)
}
}

/// A convenience type to specify a `TestScheduler` by the scheduler it wraps rather than by the
Expand Down
88 changes: 88 additions & 0 deletions Tests/CombineSchedulersTests/TestSchedulerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,94 @@ final class CombineSchedulerTests: XCTestCase {

XCTAssertEqual(value, 1)
}

func testAdvanceWithInaccuracy() {
let scheduler = DispatchQueue.inaccurate(by: .milliseconds(50))
let start = scheduler.now
var time: DispatchQueue.SchedulerTimeType?

Just(())
.delay(for: 1, scheduler: scheduler)
.sink { time = scheduler.now }
.store(in: &cancellables)

XCTAssertEqual(time, nil)

scheduler.advance(by: .milliseconds(500))

XCTAssertEqual(time, nil)

scheduler.advance(by: .milliseconds(500))

XCTAssertEqual(time, nil)

scheduler.advance(by: .milliseconds(50))

XCTAssertEqual(
time,
start
.advanced(by: .seconds(1))
.advanced(by: .milliseconds(50))
)

XCTAssertEqual(
scheduler.now,
start
.advanced(by: .seconds(1))
.advanced(by: .milliseconds(50))
)
}

func testAdvanceToWithInaccuracy() {
let scheduler = DispatchQueue.inaccurate(by: .milliseconds(50))
let start = scheduler.now
var time: DispatchQueue.SchedulerTimeType?

Just(())
.delay(for: 1, scheduler: scheduler)
.sink { time = scheduler.now }
.store(in: &self.cancellables)

XCTAssertEqual(time, nil)

scheduler.advance(to: start.advanced(by: .milliseconds(500)))

XCTAssertEqual(time, nil)

scheduler.advance(to: start.advanced(by: .milliseconds(1000)))

XCTAssertEqual(time, nil)

scheduler.advance(to: start.advanced(by: .milliseconds(1050)))

XCTAssertEqual(
time,
start
.advanced(by: .seconds(1))
.advanced(by: .milliseconds(50))
)
}

func testInaccruateAsyncTimer() async throws {
let scheduler = DispatchQueue.inaccurate(by: .milliseconds(50))
let start = scheduler.now

let task = Task {
var times = [DispatchQueue.SchedulerTimeType]()
for await time in scheduler.timer(interval: .seconds(1)).prefix(3) {
times.append(time)
}
return times
}

await scheduler.advance(by: .seconds(10))
let times = await task.value
XCTAssertEqual(times, [
start.advanced(by: .milliseconds(1050)),
start.advanced(by: .milliseconds(2050)),
start.advanced(by: .milliseconds(3050)),
])
}

func testAdvanceTo() {
let scheduler = DispatchQueue.test
Expand Down