diff --git a/Package.swift b/Package.swift index c886ceed..9f9ef685 100644 --- a/Package.swift +++ b/Package.swift @@ -63,6 +63,10 @@ let package = Package( name: "SwiftNetworkBenchmarks", targets: ["SwiftNetworkBenchmarks"] ), + .library( + name: "SwiftNetworkTestSupport", + targets: ["SwiftNetworkTestSupport"] + ), ], traits: [ .trait( @@ -144,14 +148,19 @@ let package = Package( ], swiftSettings: availabilityMacros + settings ), + .target( + name: "SwiftNetworkTestSupport", + dependencies: ["SwiftNetwork"], + swiftSettings: availabilityMacros + settings + ), .testTarget( name: "SwiftNetworkTests", - dependencies: ["SwiftNetwork", "SwiftNetworkTestHarness"], + dependencies: ["SwiftNetwork", "SwiftNetworkTestHarness", "SwiftNetworkTestSupport"], swiftSettings: availabilityMacros + settings ), .testTarget( name: "QUICTests", - dependencies: ["SwiftNetwork", "SwiftNetworkTestHarness"], + dependencies: ["SwiftNetwork", "SwiftNetworkTestHarness", "SwiftNetworkTestSupport"], swiftSettings: availabilityMacros + settings ), .executableTarget( diff --git a/Sources/SwiftNetwork/Parameters/Parameters.swift b/Sources/SwiftNetwork/Parameters/Parameters.swift index 4e25f92c..a1be732c 100644 --- a/Sources/SwiftNetwork/Parameters/Parameters.swift +++ b/Sources/SwiftNetwork/Parameters/Parameters.swift @@ -725,6 +725,12 @@ public struct Parameters: Hashable, CustomStringConvertible { } #endif + /// Creates default parameters that run on `context`, activating it. + public init(context: NetworkContext) { + self = Self.init() + self.context = context + } + public var context: NetworkContext { get { pathParameters.context } set { diff --git a/Sources/SwiftNetworkBenchmarks/BenchmarkUtility.swift b/Sources/SwiftNetworkBenchmarks/BenchmarkUtility.swift index 6d68e6c1..7c6409b0 100644 --- a/Sources/SwiftNetworkBenchmarks/BenchmarkUtility.swift +++ b/Sources/SwiftNetworkBenchmarks/BenchmarkUtility.swift @@ -156,8 +156,7 @@ public final class QUICBenchmarkUtility { logger: LoggingHandle ) throws -> QUICClientEndpointResult? { // Set context on parameters to activate the context before asyncing - var parameters = Parameters() - parameters.context = context + var parameters = Parameters(context: context) parameters.isServer = false parameters.defaultStack.transport = .quic(options) @@ -226,8 +225,7 @@ public final class QUICBenchmarkUtility { remoteEndpoint: Endpoint, logger: LoggingHandle ) throws -> QUICServerEndpointResult? { - var serverParameters = Parameters() - serverParameters.context = context + var serverParameters = Parameters(context: context) serverParameters.defaultStack.transport = .quic(options) serverParameters.isServer = true let serverPath = PathProperties(parameters: serverParameters) diff --git a/Sources/SwiftNetworkTestSupport/ManualScheduler.swift b/Sources/SwiftNetworkTestSupport/ManualScheduler.swift new file mode 100644 index 00000000..78310f37 --- /dev/null +++ b/Sources/SwiftNetworkTestSupport/ManualScheduler.swift @@ -0,0 +1,317 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(SwiftNetwork) +@_spi(Essentials) @_spi(ProtocolProvider) import SwiftNetwork +#elseif canImport(Network) +@_spi(Essentials) @_spi(ProtocolProvider) import Network +#endif + +#if canImport(Darwin) +internal import Darwin +#elseif canImport(Glibc) +internal import Glibc +#elseif canImport(Musl) +internal import Musl +#endif + +/// A `NetworkContext.Scheduler` that virtualizes time. +/// +/// This scheduler owns both the clock and the timers, because the stack arms timers rather than +/// polling the clock. Advancing time fires each timer with `now` set to that timer's own deadline, +/// so a test can drive a retransmission or an idle timeout without sleeping. +/// +/// Every instant this API takes or returns is in the continuous domain; the absolute clock follows +/// by the same delta. +/// +/// Install it with `NetworkContext(identifier:externalScheduler:)`. +/// +/// **NOTE: **For synchronous tests only: no queue and no locking. Every call must come from the thread that +/// constructed the scheduler. +@available(Network 0.1.0, *) +@_spi(Essentials) +public final class ManualScheduler: NetworkContext.Scheduler { + + /// Comparable in firing order: by deadline, then by the order the timer was armed. + private struct Timer: Comparable { + var deadline: NetworkClock.Instant + var armOrder: UInt64 + var task: () -> Void + + func isDue(by instant: NetworkClock.Instant) -> Bool { + self.deadline <= instant + } + + /// Whether this timer fires during an advance from `continuous` to `deadline`. + /// + /// A timer armed during that advance, for an instant already reached, waits for the next + /// advance rather than firing re-entrantly, matching a serial dispatch queue. + func firesDuringAdvance( + from continuous: NetworkClock.Instant, + to end: NetworkClock.Instant, + armOrderCutoff: UInt64 + ) -> Bool { + if !self.isDue(by: end) { + return false + } + if self.isDue(by: continuous), self.armOrder >= armOrderCutoff { + return false + } + return true + } + + static func < (lhs: Self, rhs: Self) -> Bool { + if lhs.deadline != rhs.deadline { + return lhs.deadline < rhs.deadline + } + return lhs.armOrder < rhs.armOrder + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.deadline == rhs.deadline && lhs.armOrder == rhs.armOrder + } + } + + private var timers: [TimerReference: Timer] = [:] + private var armOrderCounter: UInt64 = 0 + /// Work handed to `runImmediate`, waiting for the next `run()` or advance. + private var queuedWork: [() -> Void] = [] + private var continuous: NetworkClock.Instant + private var absolute: NetworkClock.Instant + + /// The thread that owns this scheduler. + private let owningThread = pthread_self() + + /// How many times `runQueuedWork` drains before concluding nothing is making progress. + private static let maximumQueuedWorkBatches = 10_000 + + /// Creates a scheduler holding both clocks, with no timers armed and no work queued. + /// + /// - Parameters: + /// - now: Must be greater than zero, because much of the stack treats `.zero` as "unset". + /// - nowAbsolute: Defaults to `now`. + @_spi(Essentials) + public init(now: NetworkClock.Instant, nowAbsolute: NetworkClock.Instant? = nil) { + precondition(now > .zero, "manual time must be greater than zero") + self.continuous = now + self.absolute = nowAbsolute ?? now + } + + @_spi(Essentials) + public var now: NetworkClock.Instant { self.continuous } + @_spi(Essentials) + public var nowAbsolute: NetworkClock.Instant { self.absolute } + + /// Whether the calling thread owns the scheduler. + @_spi(Essentials) + public var runningInScheduler: Bool { + pthread_equal(pthread_self(), self.owningThread) != 0 + } + + /// Queues `task` to run at the next `run()` or advance. + /// + /// Does not run in-line to avoid re-entrancy. + @_spi(Essentials) + public func runImmediate(_ task: @escaping (() -> Void)) { + self.queuedWork.append(task) + } + + @_spi(Essentials) + public func schedule(_ task: @escaping (() -> Void), after delay: NetworkDuration, reference: TimerReference) { + self.timers[reference] = Timer( + deadline: self.continuous.advanced(by: delay), + armOrder: self.nextArmOrder(), + task: task + ) + } + + @_spi(Essentials) + public func unschedule(reference: TimerReference) { + self.timers[reference] = nil + } + + /// Consumes an arm order, so no two timers share one. + private func nextArmOrder() -> UInt64 { + let order = self.armOrderCounter + self.armOrderCounter &+= 1 + return order + } + + /// Lets a test assert that something was scheduled, or that canceling really canceled. + @_spi(Essentials) + public var scheduledCount: Int { self.timers.count } + + /// Runs queued work and any timer already due, including whatever those queue in turn. + /// No virtual time passes. + @_spi(Essentials) + public func run() { + self.advanceTime(to: self.continuous) + } + + /// Runs queued work, including anything produced in the course of that work. + /// + /// Work queued by a task joins the back of the line rather than interrupting the batch in + /// progress, matching a serial dispatch queue. Draining in batches rather than recursing is + /// what produces that order. + /// + /// Separate from `run()` because `advanceTime(to:)` calls this method between timers and must + /// not recurse back into the timer scan partway through one. + /// + /// The loop is bounded to protect against a task that re-queues itself. + private func runQueuedWork() { + var batches = 0 + while !self.queuedWork.isEmpty { + batches &+= 1 + if batches > Self.maximumQueuedWorkBatches { + preconditionFailure( + """ + Queued work did not settle in \(Self.maximumQueuedWorkBatches) batches. \ + A task is requeuing itself without making progress; it most likely needs \ + virtual time to advance, which cannot happen while queued work is draining. + """ + ) + } + let batch = self.queuedWork + self.queuedWork.removeAll() + for task in batch { + task() + } + } + } + + /// Moves time forward by `duration`, firing every timer that comes due at its own deadline. + @_spi(Essentials) + public func advanceTime(by duration: NetworkDuration) { + precondition(duration >= .zero, "manual time must not go backwards") + self.advanceTime(to: self.continuous.advanced(by: duration)) + } + + /// Moves time forward to `deadline`, firing every timer that comes due. + /// + /// Time is set to each timer's own deadline before that timer runs, not jumped straight to + /// `deadline`, so a timer that reads `now` sees the time it was scheduled for. + @_spi(Essentials) + public func advanceTime(to deadline: NetworkClock.Instant) { + precondition(deadline >= self.continuous, "manual time must not go backwards") + + // Store the initial value so we don't fire timers armed by this drain. + var armOrderCutoff = self.armOrderCounter + + // Run queued work before we advance time. + self.runQueuedWork() + + while true { + // Folded into one pass: this scan runs once per timer fired, and filtering first would + // allocate a dictionary on every pass. + let next = self.timers.min { lhs, rhs in + let lhsFires = lhs.value.firesDuringAdvance( + from: self.continuous, + to: deadline, + armOrderCutoff: armOrderCutoff + ) + let rhsFires = rhs.value.firesDuringAdvance( + from: self.continuous, + to: deadline, + armOrderCutoff: armOrderCutoff + ) + if lhsFires != rhsFires { + return lhsFires + } + return lhs.value < rhs.value + } + guard let next, + next.value.firesDuringAdvance( + from: self.continuous, + to: deadline, + armOrderCutoff: armOrderCutoff + ) + else { + break + } + + self.timers[next.key] = nil + let instantBefore = self.continuous + self.setContinuousTime(to: next.value.deadline) + next.value.task() + // Timers armed before now are late rather than re-entrant, so let them fire. Only + // when the clock really moved: otherwise a timer that rearms itself for the same + // instant fires forever. + if self.continuous != instantBefore { + armOrderCutoff = self.armOrderCounter + } + self.runQueuedWork() + } + + self.setContinuousTime(to: deadline) + self.runQueuedWork() + } + + /// Sets the continuous clock to `instant`, moving the absolute clock by the same delta. + /// + /// NOTE: `instant`s earlier than current time are ignored. + private func setContinuousTime(to instant: NetworkClock.Instant) { + let delta = self.continuous.duration(to: instant) + guard delta > .zero else { return } + self.continuous = instant + self.absolute = self.absolute.advanced(by: delta) + } + + // MARK: - Running + + private var earliestDeadline: NetworkClock.Instant? { + self.timers.values.lazy.map(\.deadline).min() + } + + /// Drives the scheduler until `condition` holds or `limit` is exhausted, and reports which. + /// + /// Use this where a test would otherwise wait for a callback: nothing else is running to + /// signal one, so progress has to come from this call. Each round drains queued work and then, + /// if the condition still does not hold, jumps to the next armed deadline. Draining first lets + /// work already queued satisfy the condition without any time passing. + /// + /// Prefer `run()` and `advanceTime(by:)` where a test knows how much progress it expects. + /// + /// - Parameters: + /// - condition: Checked after the drain and again after the advance. + /// - limit: How much virtual time to allow before giving up. Nothing real elapses. + /// - rounds: Backstop against a condition that never holds while the scheduler keeps making + /// progress. + /// - Returns: Whether `condition` held. A `false` means the budget or the round count ran out + /// with the condition still unmet, so a caller that ignores it goes on to assert against a + /// state that never arrived. + @_spi(Essentials) + public func run( + until condition: () -> Bool, + limit: NetworkDuration = .seconds(30), + rounds: Int = 10_000 + ) -> Bool { + let deadline = self.continuous.advanced(by: limit) + + for _ in 0.. ManualScheduler { + ManualScheduler(now: self.base, nowAbsolute: nowAbsolute) + } + + // MARK: - Time + + /// Reading repeatedly must yield the same instant; otherwise a duration measured across a test + /// depends on the real clock. + func testTimeDoesNotMoveOnItsOwn() { + let scheduler = self.makeScheduler() + XCTAssertEqual(scheduler.now, self.base) + XCTAssertEqual(scheduler.now, self.base) + } + + func testAbsoluteDefaultsToContinuous() { + XCTAssertEqual(self.makeScheduler().nowAbsolute, self.base) + } + + func testContinuousAndAbsoluteAreSeparate() { + let absolute = NetworkClock.Instant(milliseconds: 5000) + let scheduler = self.makeScheduler(nowAbsolute: absolute) + XCTAssertEqual(scheduler.now, self.base) + XCTAssertEqual(scheduler.nowAbsolute, absolute) + } + + func testAdvanceMovesBothClocks() { + let absolute = NetworkClock.Instant(milliseconds: 5000) + let scheduler = self.makeScheduler(nowAbsolute: absolute) + scheduler.advanceTime(by: .milliseconds(250)) + XCTAssertEqual(scheduler.now, self.base.advanced(by: .milliseconds(250))) + XCTAssertEqual(scheduler.nowAbsolute, absolute.advanced(by: .milliseconds(250))) + } + + /// `Pacer` converts between the two domains using the offset between them, so a scheduler that + /// let them drift would make `Pacer` compute a nonsense interval. + func testAdvanceKeepsTheOffsetBetweenTheClocksFixed() { + let absolute = NetworkClock.Instant(milliseconds: 5000) + let scheduler = self.makeScheduler(nowAbsolute: absolute) + let offsetBefore = scheduler.nowAbsolute.duration(to: scheduler.now) + + scheduler.advanceTime(by: .milliseconds(250)) + + XCTAssertEqual(scheduler.nowAbsolute.duration(to: scheduler.now), offsetBefore) + } + + func testAdvanceAccumulates() { + let scheduler = self.makeScheduler() + for _ in 0..<3 { + scheduler.advanceTime(by: .milliseconds(100)) + } + XCTAssertEqual(scheduler.now, self.base.advanced(by: .milliseconds(300))) + } + + func testAdvanceByZeroLeavesTheClockAlone() { + let scheduler = self.makeScheduler() + scheduler.advanceTime(by: .zero) + XCTAssertEqual(scheduler.now, self.base) + } + + /// `System.Time.now()` truncates to microseconds, so nanosecond steps are only observable on a + /// manual clock. + func testNanosecondResolutionIsPreserved() { + let scheduler = ManualScheduler(now: NetworkClock.Instant(nanoseconds: 1)) + scheduler.advanceTime(by: .nanoseconds(1)) + XCTAssertEqual(scheduler.now.time, .nanoseconds(2)) + } + + /// Elapsed time must be exact, with no dependency on how long the test itself took to run. + func testDurationIsExactAcrossAdvances() { + let scheduler = self.makeScheduler() + let start = scheduler.now + scheduler.advanceTime(by: .milliseconds(5)) + XCTAssertEqual(start.duration(to: scheduler.now), .milliseconds(5)) + } + + // MARK: - Timers + + /// Advancing the clock must fire a timer that comes due. Nothing in the stack polls the clock, + /// so a timer only runs because the scheduler ran it. + func testAdvanceFiresADueTimer() { + let scheduler = self.makeScheduler() + var fired = 0 + scheduler.schedule({ fired += 1 }, after: .milliseconds(100), reference: TimerReference()) + XCTAssertEqual(fired, 0) + + scheduler.advanceTime(by: .milliseconds(100)) + + XCTAssertEqual(fired, 1) + } + + /// `run()` must fire a timer whose deadline has already arrived. A zero-delay wakeup is + /// reachable from production: `Timer.recalculate` clamps an overdue deadline to `.zero`. If + /// `run()` skips it, the timer waits for an advance the test has no reason to make. + func testRunFiresATimerThatIsAlreadyDue() { + let scheduler = self.makeScheduler() + var fired = 0 + scheduler.schedule({ fired += 1 }, after: .zero, reference: TimerReference()) + + scheduler.run() + + XCTAssertEqual(fired, 1) + XCTAssertEqual(scheduler.now, self.base, "firing a due timer must not invent virtual time") + } + + func testATimerNotYetDueDoesNotFire() { + let scheduler = self.makeScheduler() + var fired = 0 + scheduler.schedule({ fired += 1 }, after: .milliseconds(100), reference: TimerReference()) + scheduler.advanceTime(by: .milliseconds(99)) + XCTAssertEqual(fired, 0) + XCTAssertEqual(scheduler.scheduledCount, 1) + } + + /// A delay under a millisecond must keep its own deadline; otherwise it arrives as a zero + /// delay. The wakeup has then already passed its deadline, so the handler recomputes the same + /// delay and arms again forever. + func testASubMillisecondDelayKeepsItsDeadline() { + let scheduler = self.makeScheduler() + var fired = 0 + scheduler.schedule({ fired += 1 }, after: .microseconds(625), reference: TimerReference()) + + scheduler.advanceTime(by: .microseconds(624)) + XCTAssertEqual(fired, 0) + + scheduler.advanceTime(by: .microseconds(1)) + XCTAssertEqual(fired, 1) + XCTAssertEqual(scheduler.now, self.base.advanced(by: .microseconds(625))) + } + + /// A timer must observe its own deadline, not the end of the advance; otherwise any duration it + /// measures against `now` is wrong. + func testATimerSeesTheTimeItWasScheduledFor() { + let scheduler = self.makeScheduler() + var observed: NetworkClock.Instant? + scheduler.schedule({ observed = scheduler.now }, after: .milliseconds(100), reference: TimerReference()) + + // Advance well past the deadline in one step. + scheduler.advanceTime(by: .milliseconds(500)) + + XCTAssertEqual(observed, self.base.advanced(by: .milliseconds(100))) + XCTAssertEqual(scheduler.now, self.base.advanced(by: .milliseconds(500))) + } + + func testTimersFireInDeadlineOrder() { + let scheduler = self.makeScheduler() + var order: [String] = [] + scheduler.schedule({ order.append("late") }, after: .milliseconds(200), reference: TimerReference()) + scheduler.schedule({ order.append("early") }, after: .milliseconds(50), reference: TimerReference()) + + scheduler.advanceTime(by: .milliseconds(300)) + + XCTAssertEqual(order, ["early", "late"]) + } + + /// Ordering must come from the order the timers were armed, not from however the scheduler + /// happens to store them. + func testTimersWithEqualDeadlinesFireInArmOrder() { + let scheduler = self.makeScheduler() + var order: [Int] = [] + // Same deadline for all three. + for index in 0..<3 { + scheduler.schedule( + { order.append(index) }, + after: .milliseconds(100), + reference: TimerReference() + ) + } + + scheduler.advanceTime(by: .milliseconds(100)) + + XCTAssertEqual(order, [0, 1, 2]) + } + + func testUnscheduleCancelsATimer() { + let scheduler = self.makeScheduler() + var fired = 0 + let reference = TimerReference() + scheduler.schedule({ fired += 1 }, after: .milliseconds(100), reference: reference) + scheduler.unschedule(reference: reference) + + scheduler.advanceTime(by: .milliseconds(500)) + + XCTAssertEqual(fired, 0) + XCTAssertEqual(scheduler.scheduledCount, 0) + } + + func testAFiredTimerDoesNotFireTwice() { + let scheduler = self.makeScheduler() + var fired = 0 + scheduler.schedule({ fired += 1 }, after: .milliseconds(100), reference: TimerReference()) + scheduler.advanceTime(by: .milliseconds(100)) + scheduler.advanceTime(by: .milliseconds(100)) + XCTAssertEqual(fired, 1) + } + + /// Rearming from inside a callback is what the QUIC timers do, so the advance has to keep + /// draining rather than taking one pass. + func testATimerScheduledByATimerAlsoFires() { + let scheduler = self.makeScheduler() + var inner = 0 + scheduler.schedule( + { scheduler.schedule({ inner += 1 }, after: .milliseconds(50), reference: TimerReference()) }, + after: .milliseconds(100), + reference: TimerReference() + ) + + scheduler.advanceTime(by: .milliseconds(300)) + + XCTAssertEqual(inner, 1) + } + + // MARK: - Immediate work + + /// `runImmediate` must queue rather than run inline; otherwise it re-enters the caller, which + /// a serial dispatch queue never does. + func testImmediateWorkIsQueuedNotRunInline() { + let scheduler = self.makeScheduler() + var ran = false + scheduler.runImmediate { ran = true } + XCTAssertFalse(ran) + scheduler.run() + XCTAssertTrue(ran) + } + + // MARK: - Wiring into a context + + /// Everything downstream reads time through `context.scheduler`, so a context handed a manual + /// scheduler must report that scheduler's time; otherwise controlling the scheduler here + /// controls nothing. + func testAContextUsesTheSuppliedScheduler() { + let scheduler = self.makeScheduler() + let context = NetworkContext(identifier: "manual", externalScheduler: scheduler) + + XCTAssertEqual(context.scheduler.now, self.base) + + scheduler.advanceTime(by: .seconds(1)) + + XCTAssertEqual(context.scheduler.now, self.base.advanced(by: .seconds(1))) + } + + /// A context given no scheduler must still read the real clock, which is what every existing + /// test depends on. + func testTheDefaultContextStillUsesTheRealClock() { + let context = NetworkContext(identifier: "default") + XCTAssertNotEqual(context.scheduler.now, .zero) + } + + /// Work submitted before an advance belongs to the instant the caller is standing on, so it + /// must run before a timer armed for later; otherwise a `runImmediate` is silently overtaken. + func testQueuedWorkRunsBeforeATimerThatComesDueDuringTheAdvance() { + let scheduler = self.makeScheduler() + var order: [String] = [] + + scheduler.schedule({ order.append("timer") }, after: .milliseconds(10), reference: TimerReference()) + scheduler.runImmediate { order.append("queued") } + + scheduler.advanceTime(by: .milliseconds(10)) + + XCTAssertEqual(order, ["queued", "timer"]) + } + + /// A timer that rearms itself for the instant it just fired on must not spin the advance. The + /// hold-back is what bounds it, so releasing the hold as the clock moves must not release this. + func testASelfRearmingZeroDelayTimerDoesNotSpinTheAdvance() { + let scheduler = self.makeScheduler() + var fired = 0 + func rearm() { + fired += 1 + if fired < 100_000 { + scheduler.schedule({ rearm() }, after: .zero, reference: TimerReference()) + } + } + scheduler.schedule({ rearm() }, after: .zero, reference: TimerReference()) + + scheduler.advanceTime(by: .milliseconds(1)) + + XCTAssertLessThan(fired, 100_000, "a timer rearming itself for the same instant spun the advance") + } + + /// A timer armed part-way through an advance, for an instant the clock has already reached, must + /// fire once the clock moves past it. Holding it for the rest of the advance leaves it arbitrarily + /// overdue -- here 990ms -- and a serial dispatch queue would have run it on the next turn. + func testATimerArmedMidAdvanceFiresOnceTheClockHasMoved() { + let scheduler = self.makeScheduler() + var inner = 0 + // The outer timer fires 10ms in and arms a zero-delay one; the advance runs on to 1000ms. + scheduler.schedule( + { scheduler.schedule({ inner += 1 }, after: .zero, reference: TimerReference()) }, + after: .milliseconds(10), + reference: TimerReference() + ) + + scheduler.advanceTime(by: .seconds(1)) + + XCTAssertEqual(inner, 1, "a zero-delay timer armed mid-advance never fired") + } + + /// A zero-delay timer armed by work that drains at the start of an advance must wait for the + /// next advance. Firing it inside the advance that armed it is re-entrancy, which a serial + /// dispatch queue never does. + func testATimerArmedByTheOpeningDrainWaitsForTheNextAdvance() { + let scheduler = self.makeScheduler() + var fired = 0 + + scheduler.runImmediate { + scheduler.schedule({ fired += 1 }, after: .zero, reference: TimerReference()) + } + + scheduler.advanceTime(by: .milliseconds(10)) + XCTAssertEqual(fired, 0, "the timer fired inside the advance that armed it") + + scheduler.run() + XCTAssertEqual(fired, 1, "the timer never fired on a later advance") + } +} diff --git a/Tests/SwiftNetworkTests/QUICTestHarness.swift b/Tests/SwiftNetworkTests/QUICTestHarness.swift index 2c7e90b2..2b58b1b7 100644 --- a/Tests/SwiftNetworkTests/QUICTestHarness.swift +++ b/Tests/SwiftNetworkTests/QUICTestHarness.swift @@ -174,8 +174,7 @@ class QUICTestHarness { // Async onto the context to attach the protocol stack and start the handshake context.async { // Setup client parameters - var clientParameters = Parameters() - clientParameters.context = self.context + var clientParameters = Parameters(context: self.context) clientParameters.isServer = false // Build the QUIC connection through storage so its listener/multipath linkages are @@ -210,8 +209,7 @@ class QUICTestHarness { clientPath.effectiveMTU = 1500 // Setup server parameters - var serverParameters = Parameters() - serverParameters.context = self.context + var serverParameters = Parameters(context: self.context) serverParameters.isServer = true var (serverQUICStreamListener, serverQUICDatagramListener, serverQUICMultipath) = @@ -462,8 +460,7 @@ class QUICTestHarness { return nil } var upperHarnessConnected = false - var parameters = Parameters() - parameters.context = context + let parameters = Parameters(context: context) var upperHarness: StreamUpperHarness? let newStreamExpectation = XCTestExpectation(description: "Wait for new QUIC stream to be ready") @@ -545,8 +542,7 @@ class QUICTestHarness { return nil } var upperHarnessConnected = false - var parameters = Parameters() - parameters.context = context + let parameters = Parameters(context: context) var upperHarness: DatagramUpperHarness? let newFlowExpectation = XCTestExpectation(description: "Wait for new QUIC datagram flow to be ready") @@ -1993,8 +1989,7 @@ class QUICTestHarness { let maxStreamsUpdateExpectation = XCTestExpectation(description: "Wait for MAX_STREAMS update to be processed") context.async { // Create a new stream by hand, this should put us over the remote max streams limit - var parameters = Parameters() - parameters.context = self.context + let parameters = Parameters(context: self.context) let options = QUICProtocol.options() options.setLogID( prefix: identifier, diff --git a/Tests/SwiftNetworkTests/SwiftNetworkDemuxTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkDemuxTests.swift index 1c62b650..f4359583 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkDemuxTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkDemuxTests.swift @@ -122,8 +122,7 @@ final class SwiftNetworkDemuxTests: NetTestCase { context.async { defer { expectation.fulfill() } - var parameters = Parameters() - parameters.context = context + let parameters = Parameters(context: context) let localEndpoint = Endpoint(address: IPv4Address(Self.localIPv4Address)!, port: 1234) let remoteEndpoint = Endpoint(address: IPv4Address(Self.remoteIPv4Address)!, port: 8080) @@ -204,8 +203,7 @@ final class SwiftNetworkDemuxTests: NetTestCase { [(harness: DatagramUpperHarness, patterns: [DemuxPatternInput])] = [] for demuxedFlow in demuxedFlows { - var demuxParameters = Parameters() - demuxParameters.context = context + let demuxParameters = Parameters(context: context) guard !demuxedFlow.isEmpty else { continue } diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICEarlyDataTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICEarlyDataTests.swift index fa697410..65e385af 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICEarlyDataTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICEarlyDataTests.swift @@ -165,11 +165,9 @@ final class SwiftNetworkQUICEarlyDataTests: NetTestCase { serverIPOptions.setLogID(prefix: "L", parent: identifier, protocolLogIDNumber: 3) serverIPOptions.setProtocolInstance(serverIPLower.identifier) - var clientParameters = Parameters() - clientParameters.context = context + let clientParameters = Parameters(context: context) - var serverParameters = Parameters() - serverParameters.context = context + let serverParameters = Parameters(context: context) let clientPath = PathProperties(parameters: clientParameters) let serverPath = PathProperties(parameters: serverParameters) diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICPacketParsingTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICPacketParsingTests.swift index 6b647ee8..561a3280 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICPacketParsingTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICPacketParsingTests.swift @@ -252,8 +252,7 @@ final class SwiftNetworkQUICPacketParsingTests: NetTestCase { var result: IdleServer? harness.context.async { - var serverParameters = Parameters() - serverParameters.context = harness.context + var serverParameters = Parameters(context: harness.context) serverParameters.isServer = true var (serverQUICStreamListener, _, serverQUICMultipath) = harness.storage.createTestQUICInstance() diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICStackTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICStackTests.swift index 42649bdd..a2754127 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICStackTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICStackTests.swift @@ -163,11 +163,9 @@ final class SwiftNetworkQUICStackTests: NetTestCase { serverIPOptions.setLogID(prefix: "L", parent: identifier, protocolLogIDNumber: 3) serverIPOptions.setProtocolInstance(serverIPLower.identifier) - var clientParameters = Parameters() - clientParameters.context = context + let clientParameters = Parameters(context: context) - var serverParameters = Parameters() - serverParameters.context = context + let serverParameters = Parameters(context: context) let clientPath = PathProperties(parameters: clientParameters) let serverPath = PathProperties(parameters: serverParameters)