mirror of
https://github.com/aculix/knotchy.git
synced 2026-10-27 07:15:53 +00:00
157 lines
5.4 KiB
Swift
157 lines
5.4 KiB
Swift
import Testing
|
|
@testable import KnotchyCore
|
|
|
|
@Suite(.timeLimit(.minutes(1)))
|
|
struct TestClockTests {
|
|
@Test func sleepReturnsOnlyOnceTimeIsAdvancedPastTheDeadline() async throws {
|
|
let clock = TestClock()
|
|
let deadline = clock.now + .seconds(10)
|
|
let sleeper = Task { try await clock.sleep(until: deadline) }
|
|
await waitUntil("the task sleeps") { clock.sleeperCount == 1 }
|
|
|
|
clock.advance(by: .seconds(9))
|
|
#expect(clock.sleeperCount == 1)
|
|
|
|
clock.advance(by: .seconds(1))
|
|
try await sleeper.value
|
|
#expect(clock.sleeperCount == 0)
|
|
}
|
|
|
|
@Test func sleepingUntilNowOrThePastReturnsImmediately() async throws {
|
|
let clock = TestClock()
|
|
try await clock.sleep(until: clock.now)
|
|
try await clock.sleep(until: clock.now - .seconds(1))
|
|
}
|
|
|
|
@MainActor
|
|
@Test func aSleepInAnAlreadyCancelledTaskThrows() async {
|
|
@MainActor final class Outcome {
|
|
var error: (any Error)?
|
|
var finished = false
|
|
}
|
|
let clock = TestClock()
|
|
let outcome = Outcome()
|
|
Task { @MainActor in
|
|
withUnsafeCurrentTask { $0?.cancel() }
|
|
do { try await clock.sleep(until: clock.now + .seconds(10)) } catch { outcome.error = error }
|
|
outcome.finished = true
|
|
}
|
|
// Bounded: a sleep that wrongly registers would never return.
|
|
await waitUntil("the sleep ends") { outcome.finished }
|
|
#expect(outcome.error is CancellationError)
|
|
#expect(clock.sleeperCount == 0)
|
|
}
|
|
|
|
@Test func cancellingASleeperThrowsAndForgetsIt() async {
|
|
let clock = TestClock()
|
|
let sleeper = Task { try await clock.sleep(until: clock.now + .seconds(10)) }
|
|
await waitUntil("the task sleeps") { clock.sleeperCount == 1 }
|
|
|
|
sleeper.cancel()
|
|
await #expect(throws: CancellationError.self) { try await sleeper.value }
|
|
#expect(clock.sleeperCount == 0)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
struct DeadlineTimersTests {
|
|
@Test func firesDueDeadlinesInOrder() {
|
|
let clock = TestClock()
|
|
let timers = DeadlineTimers<String>(clock: clock)
|
|
var fired: [String] = []
|
|
timers.onFire = { fired.append($0) }
|
|
|
|
// Seven keys share a deadline, so a lost tie-break shows up nearly every run.
|
|
let ties = (1...6).map { "tie\($0)" }
|
|
timers.set("late", after: .seconds(2))
|
|
timers.set("early", after: .seconds(1))
|
|
for key in ties { timers.set(key, after: .seconds(2)) }
|
|
clock.advance(by: .milliseconds(999))
|
|
timers.fireDue()
|
|
#expect(fired == [])
|
|
|
|
clock.advance(by: .seconds(2))
|
|
timers.fireDue()
|
|
#expect(fired == ["early", "late"] + ties)
|
|
|
|
timers.fireDue()
|
|
#expect(fired == ["early", "late"] + ties)
|
|
}
|
|
|
|
@Test func settingAKeyAgainReplacesItsDeadline() {
|
|
let clock = TestClock()
|
|
let timers = DeadlineTimers<String>(clock: clock)
|
|
var fired: [String] = []
|
|
timers.onFire = { fired.append($0) }
|
|
|
|
timers.set("a", after: .seconds(1))
|
|
timers.set("a", after: .seconds(5))
|
|
#expect(timers.deadline(of: "a") == clock.now + .seconds(5))
|
|
clock.advance(by: .seconds(4))
|
|
timers.fireDue()
|
|
#expect(fired == [])
|
|
}
|
|
|
|
@Test func cancelledDeadlinesNeverFire() {
|
|
let clock = TestClock()
|
|
let timers = DeadlineTimers<String>(clock: clock)
|
|
var fired: [String] = []
|
|
timers.onFire = { fired.append($0) }
|
|
|
|
timers.set("a", after: .seconds(1))
|
|
timers.cancel("a")
|
|
clock.advance(by: .seconds(2))
|
|
timers.fireDue()
|
|
#expect(fired == [])
|
|
#expect(timers.deadline(of: "a") == nil)
|
|
}
|
|
|
|
@Test func onFireMaySetAndCancelTimers() {
|
|
let clock = TestClock()
|
|
let timers = DeadlineTimers<String>(clock: clock)
|
|
var fired: [String] = []
|
|
timers.onFire = { [unowned timers] key in
|
|
fired.append(key)
|
|
if key == "a" {
|
|
timers.cancel("b")
|
|
timers.set("c", after: .zero)
|
|
timers.set("d", after: .seconds(1))
|
|
}
|
|
}
|
|
timers.set("a", after: .seconds(1))
|
|
timers.set("b", after: .seconds(1))
|
|
clock.advance(by: .seconds(1))
|
|
timers.fireDue()
|
|
#expect(fired == ["a", "c"])
|
|
#expect(timers.deadline(of: "d") == clock.now + .seconds(1))
|
|
}
|
|
|
|
@Test func theDriverFiresEveryTimerInDeadlineOrder() async {
|
|
let clock = TestClock()
|
|
let timers = DeadlineTimers<String>(clock: clock)
|
|
var fired: [String] = []
|
|
timers.onFire = { fired.append($0) }
|
|
|
|
timers.set("b", after: .seconds(2))
|
|
timers.set("a", after: .seconds(1))
|
|
await waitUntil("the driver sleeps") { clock.sleeperCount == 1 }
|
|
clock.advance(by: .seconds(1))
|
|
await waitUntil("the driver fires a") { fired == ["a"] }
|
|
await waitUntil("the driver sleeps again") { clock.sleeperCount == 1 }
|
|
clock.advance(by: .seconds(1))
|
|
await waitUntil("the driver fires b") { fired == ["a", "b"] }
|
|
}
|
|
|
|
@Test func theDriverFiresWithoutHelp() async {
|
|
let clock = TestClock()
|
|
let timers = DeadlineTimers<String>(clock: clock)
|
|
var fired: [String] = []
|
|
timers.onFire = { fired.append($0) }
|
|
|
|
timers.set("a", after: .seconds(1))
|
|
await waitUntil("the driver sleeps") { clock.sleeperCount == 1 }
|
|
clock.advance(by: .seconds(1))
|
|
await waitUntil("the driver fires") { fired == ["a"] }
|
|
}
|
|
}
|