diff --git a/.github/workflows/scripts/ci-linux.sh b/.github/workflows/scripts/ci-linux.sh index 6fb476cc..e2381bbf 100644 --- a/.github/workflows/scripts/ci-linux.sh +++ b/.github/workflows/scripts/ci-linux.sh @@ -35,8 +35,8 @@ ci_run 'Build (reductive traits on)' \ swift build --build-tests --quiet \ --traits DisableDebugLogging,DisableErrorLogging "$@" ci_run 'Test (debug)' \ - swift test --quiet -Xswiftc -DNETWORK_INTERNAL_TESTS "$@" + swift test --quiet "$@" ci_run 'Test (release)' \ - swift test --quiet -c release -Xswiftc -DNETWORK_INTERNAL_TESTS "$@" + swift test --quiet -c release "$@" ci_finish diff --git a/.github/workflows/scripts/ci-macos.sh b/.github/workflows/scripts/ci-macos.sh index 8af8d8e6..85ecbc4b 100644 --- a/.github/workflows/scripts/ci-macos.sh +++ b/.github/workflows/scripts/ci-macos.sh @@ -34,6 +34,6 @@ xcodebuild_step 'watchOS build' 'generic/platform=watchos' xcodebuild_step 'tvOS build' 'generic/platform=tvos' xcodebuild_step 'visionOS build' 'generic/platform=visionos' -ci_run 'swift test' xcrun swift test --quiet -Xswiftc -DNETWORK_INTERNAL_TESTS "$@" +ci_run 'swift test' xcrun swift test --quiet "$@" ci_finish diff --git a/Package.swift b/Package.swift index 083711ed..357ff54f 100644 --- a/Package.swift +++ b/Package.swift @@ -40,13 +40,6 @@ let allApplePlatforms: [Platform] = [ // Logging levels, qlog output, and QUIC signposts are configured via package // traits. See the `traits:` list on the `Package(...)` initializer below. -// -// Test-only hooks in the library, are guarded by `NETWORK_INTERNAL_TESTS`. -// Pass it on the command line instead: -// -// swift test -Xswiftc -DNETWORK_INTERNAL_TESTS -// -// Tests that depend on those hooks skip themselves when it is absent. let settings: [SwiftSetting] = [ .define("IMPORT_SWIFTTLS"), .define("EXPORT_SWIFTTLS"), diff --git a/README.md b/README.md index 3b854cb4..bc10ce57 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,6 @@ Unit tests can also be run by filtering a specific class or function: % swift test --filter SwiftNetworkUDPTests.testUDPEcho ``` -Some tests depend on hooks that are compiled into the library only on demand, because they cost a small amount of performance on hot paths. -Pass the define on the command line to compile them in: - -``` -% swift test -Xswiftc -DNETWORK_INTERNAL_TESTS -``` - All unit tests are run automatically upon creation or update of a Pull Request. See [CONTRIBUTING](https://github.com/apple/swift-network-evolution/blob/main/CONTRIBUTING.md) for details. ### Versioning diff --git a/Sources/SwiftNetwork/Context/NetworkContext.swift b/Sources/SwiftNetwork/Context/NetworkContext.swift index c4f6de76..3815a8bc 100644 --- a/Sources/SwiftNetwork/Context/NetworkContext.swift +++ b/Sources/SwiftNetwork/Context/NetworkContext.swift @@ -88,6 +88,17 @@ public final class NetworkContext: NetworkContextProtocol, @unchecked Sendable { func unschedule(reference: TimerReference) /// A Boolean value that indicates whether the current code is running in the scheduler. var runningInScheduler: Bool { get } + /// The scheduler's current notion of the continuous clock. + /// + /// The scheduler owns time because it owns the timers: one that holds scheduled tasks + /// instead of arming an OS timer has to report the time it fires them at, or a deadline it + /// just ran would still look like it is in the future. + var now: NetworkClock.Instant { get } + /// The scheduler's current notion of the absolute clock. + /// + /// Separate from `now` because the two clocks diverge across system sleep, and `Pacer` + /// reads the difference between them as a domain offset. + var nowAbsolute: NetworkClock.Instant { get } } /// Indicates the privacy level for the context. @@ -389,6 +400,13 @@ extension NetworkContext { // TODO: Not supported by DispatchQueue fatalError("Unsupported") } + /// The real clock. This is the one scheduler allowed to read it. + var now: NetworkClock.Instant { + NetworkClock.Instant.systemNow + } + var nowAbsolute: NetworkClock.Instant { + NetworkClock.Instant.systemNowAbsolute + } } } @@ -441,6 +459,19 @@ extension NetworkContext { } #if !NETWORK_PRIVATE || NETWORK_STANDALONE + /// The context's current notion of the continuous clock. + /// + /// Time comes from whatever runs the context's timers, so a scheduler that holds scheduled + /// tasks instead of arming an OS timer reports the time it fires them at. + var now: NetworkClock.Instant { + scheduler.now + } + + /// The context's current notion of the absolute clock. + var nowAbsolute: NetworkClock.Instant { + scheduler.nowAbsolute + } + func resetTimer(for reference: TimerReference, to time: FutureTime) { switch time { case .unschedule: diff --git a/Sources/SwiftNetwork/Protocols/IPProtocol.swift b/Sources/SwiftNetwork/Protocols/IPProtocol.swift index af9ca58c..269c02a5 100644 --- a/Sources/SwiftNetwork/Protocols/IPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/IPProtocol.swift @@ -2044,7 +2044,7 @@ public struct IPProtocol: NetworkProtocol { &self.instanceType, log: self.log, frames: &inboundFrames, - now: NetworkClock.Instant.now + now: self.context.now ) guard !inboundFrames.isEmpty else { log.error("Dropped inbound packets, checking for more") diff --git a/Sources/SwiftNetwork/QUIC/Pacer.swift b/Sources/SwiftNetwork/QUIC/Pacer.swift index 18785031..ebd771aa 100644 --- a/Sources/SwiftNetwork/QUIC/Pacer.swift +++ b/Sources/SwiftNetwork/QUIC/Pacer.swift @@ -60,8 +60,8 @@ struct Pacer: ~Copyable { guard let path else { return } - let continuousTime = NetworkClock.Instant.now - let absoluteTime: NetworkClock.Instant = .nowAbsolute + let continuousTime = path.parentProtocol.now + let absoluteTime = path.parentProtocol.nowAbsolute if packetSentTime == .zero { packetSentTime = absoluteTime diff --git a/Sources/SwiftNetwork/QUIC/QLog.swift b/Sources/SwiftNetwork/QUIC/QLog.swift index f07f7c94..3fe366b2 100644 --- a/Sources/SwiftNetwork/QUIC/QLog.swift +++ b/Sources/SwiftNetwork/QUIC/QLog.swift @@ -588,11 +588,14 @@ final class QLog { private var eventsList: [Event] private var topLevelObject: [String: Any] private var disableTimestamps: Bool = false - private let startTime = NetworkClock.Instant.now + private let context: NetworkContext + private let startTime: NetworkClock.Instant var configuration: QLogConfiguration? - public init(configuration: QLogConfiguration? = nil) { + public init(configuration: QLogConfiguration? = nil, context: NetworkContext) { self.configuration = configuration + self.context = context + self.startTime = context.now self.eventsList = [] self.topLevelObject = [:] self.topLevelObject["qlog_version"] = "draft-01" @@ -734,8 +737,9 @@ final class QLog { bytesInFlight: UInt64 = UInt64.max, slowStartThresh: UInt64 = UInt64.max, packetsInFlight: UInt64 = UInt64.max, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant? = nil ) { + let timestamp = timestamp ?? context.now metricsUpdated( minRTT: .zero, smoothedRTT: .zero, @@ -800,8 +804,9 @@ final class QLog { oldState: QLogCongestionState?, newState: QLogCongestionState?, trigger: QLogCongestionTrigger?, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant? = nil ) { + let timestamp = timestamp ?? context.now let congestionEvent = EventCongestionStateUpdated( oldCongestionState: oldState, newCongestionState: newState, diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index 0969cf36..3c8cd75c 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -254,6 +254,12 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, var currentInboundReceiveTimestamp: NetworkClock.Instant? var currentSendTimestamp: NetworkClock.Instant? + /// The absolute-clock reading taken alongside whichever timestamp above is set. + /// + /// `Pacer` converts between the two clock domains by subtracting one reading from the other, so + /// the pair must be sampled together; sampled a moment apart, the gap between the reads folds + /// into the offset. Stamping it here also keeps `getSendTime` from reading either clock. + var currentAbsoluteTimestamp: NetworkClock.Instant? @_optimize(speed) @inline(always) @@ -263,10 +269,16 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } else if let currentSendTimestamp { return currentSendTimestamp } else { - return NetworkClock.Instant.now + return context.now } } + @_optimize(speed) + @inline(always) + var nowAbsolute: NetworkClock.Instant { + currentAbsoluteTimestamp ?? context.nowAbsolute + } + var lastPacketReceivedTimestamp: NetworkClock.Instant = .zero var lastAckElicitingPacketSentTimestamp: NetworkClock.Instant = .zero var lastShorthandTimestamp: NetworkClock.Instant = .zero @@ -592,7 +604,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } // Only setup qlog if the directory is set if let qlogConfiguration { - self.qLog = QLog(configuration: qlogConfiguration) + self.qLog = QLog(configuration: qlogConfiguration, context: context) log.info("qlog setup with configuration: \(qlogConfiguration)") } #endif @@ -1562,8 +1574,9 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, public func serviceReceivedDatagrams(path pathID: MultiplexingPathIdentifier) { let inboundInterval = QUICSignpost.inboundStarting(id: signpostID) - // Save a timestamp to avoid calculating `now` again during processing - currentInboundReceiveTimestamp = .now + // Save the timestamps to avoid calculating `now` again during processing + currentInboundReceiveTimestamp = context.now + currentAbsoluteTimestamp = context.nowAbsolute // Start anew with pendingItems for applicationPendingItems // Detect if any received packet contains a QUIC Frame that unblocks @@ -1577,6 +1590,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, defer { recovery.endBatch(connection: self) currentInboundReceiveTimestamp = nil + currentAbsoluteTimestamp = nil } if !pendingReassemblyDequeue.isEmpty { @@ -2411,12 +2425,14 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, let outboundInterval = QUICSignpost.outboundStarting(id: signpostID) - // Save a timestamp to avoid calculating `now` again during processing - currentSendTimestamp = .now + // Save the timestamps to avoid calculating `now` again during processing + currentSendTimestamp = context.now + currentAbsoluteTimestamp = context.nowAbsolute defer { // Always reset QUICSignpost.outboundStopping(outboundInterval) currentSendTimestamp = nil + currentAbsoluteTimestamp = nil } accessStreamDataToSend(flow: flowID) { streamData in @@ -3324,10 +3340,12 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } else if packetBurst >= Constants.packetBurstCount { // The packet burst count has been reached, check the time // - // Deliberately the live clock rather than `self.now`: under a batch `self.now` is + // Deliberately the context's clock rather than `self.now`: under a batch `self.now` is // pinned, and `startSendingTimestamp` came from it, so comparing the two would // always give zero and the cap could never be reached. - if startSendingTimestamp.duration(to: .now) >= Constants.maxPacketBurstDuration { + if startSendingTimestamp.duration(to: context.now) + >= Constants.maxPacketBurstDuration + { // The maximum burst time has been reached shouldEndBurst = true } else { diff --git a/Sources/SwiftNetwork/Utilities/NetworkClock.swift b/Sources/SwiftNetwork/Utilities/NetworkClock.swift index c55c3aba..c05ed6e7 100644 --- a/Sources/SwiftNetwork/Utilities/NetworkClock.swift +++ b/Sources/SwiftNetwork/Utilities/NetworkClock.swift @@ -241,70 +241,20 @@ public struct NetworkDuration: DurationProtocol, Hashable, Equatable, CustomStri } } -/// A continuous clock with a compact representation that tests can advance manually. +/// A continuous clock with a compact representation. /// -/// Mimics `Swift.ContinuousClock`, with two differences: -/// 1. It uses `NetworkDuration` internally so its size is 8 bytes. -/// 2. Tests can replace the OS clock with one they advance by hand, -/// which makes time-dependent behaviour deterministic. +/// Mimics `Swift.ContinuousClock`, except that it uses `NetworkDuration` internally so its size is +/// 8 bytes. A test makes time deterministic by supplying a scheduler that reports instants of its +/// own, not by replacing this clock. #if !NETWORK_EMBEDDED @_spi(Essentials) -// Availability due to `SwiftNetwork`'s `System.Time` (used by `Instant.now`) +// Availability due to `SwiftNetwork`'s `System.Time` (used by `Instant.systemNow`) @available(Network 0.1.0, *) #endif public struct NetworkClock: Clock { public struct Instant: InstantProtocol, CustomStringConvertible { var time: NetworkDuration - #if NETWORK_INTERNAL_TESTS - // Backing storage for the manual clock used by tests. - // - // This is a `static let` box rather than a `static var` on purpose. - // Reading a mutable static emits a `swift_beginAccess` call for the - // dynamic exclusivity check. A `let` does not. - private final class ManualTime: @unchecked Sendable { - var continuous: Instant = .zero - var absolute: Instant = .zero - } - private static let manualTime = ManualTime() - #endif - - internal static func useSystemTime() { - #if NETWORK_INTERNAL_TESTS - manualTime.continuous = .zero - manualTime.absolute = .zero - #endif - } - - internal static func useManualTime( - _ continuous: Instant, - absolute: Instant? = nil - ) { - #if NETWORK_INTERNAL_TESTS - let absolute = absolute ?? continuous - precondition(continuous > .zero, "manual time must be greater than zero") - precondition(absolute > .zero, "manual time must be greater than zero") - manualTime.continuous = continuous - manualTime.absolute = absolute - #else - fatalError("The manual clock requires building with -DNETWORK_INTERNAL_TESTS") - #endif - } - - internal static func advanceManualTime(by duration: NetworkDuration) { - #if NETWORK_INTERNAL_TESTS - precondition(duration >= .zero, "manual time must not go backwards") - precondition( - manualTime.continuous > .zero, - "advanceManualTime(by:) requires useManualTime() first" - ) - manualTime.continuous = manualTime.continuous.advanced(by: duration) - manualTime.absolute = manualTime.absolute.advanced(by: duration) - #else - fatalError("The manual clock requires building with -DNETWORK_INTERNAL_TESTS") - #endif - } - public func advanced(by duration: NetworkDuration) -> Self { NetworkClock.Instant(self.time + duration) } @@ -345,24 +295,18 @@ public struct NetworkClock: Clock { self.time = time } - public static var now: Instant { - #if NETWORK_INTERNAL_TESTS - let manual = manualTime.continuous - if _slowPath(manual != .zero) { - return manual - } - #endif - return Instant(microseconds: Int64(System.Time.now())) + /// The system continuous clock, which no test can control. + /// + /// Reach for the context's `now` instead, or an instant the caller already holds. + /// `NetworkContext.DefaultScheduler` is the one reader of this property in the library, so a + /// test that supplies an external scheduler reports a time of its own. + package static var systemNow: Instant { + Instant(microseconds: Int64(System.Time.now())) } - public static var nowAbsolute: Instant { - #if NETWORK_INTERNAL_TESTS - let manual = manualTime.absolute - if _slowPath(manual != .zero) { - return manual - } - #endif - return Instant(nanoseconds: Int64(System.Time.nowAbsoluteNanoseconds())) + /// The system absolute clock. See `systemNow`. + package static var systemNowAbsolute: Instant { + Instant(nanoseconds: Int64(System.Time.nowAbsoluteNanoseconds())) } public static var zero: Instant { @@ -382,7 +326,7 @@ public struct NetworkClock: Clock { } public var now: Instant { - Instant.now + Instant.systemNow } public var minimumResolution: NetworkDuration { diff --git a/Sources/Tools/QUICStreamLoad/main.swift b/Sources/Tools/QUICStreamLoad/main.swift index 2006fc1c..d59b78ec 100644 --- a/Sources/Tools/QUICStreamLoad/main.swift +++ b/Sources/Tools/QUICStreamLoad/main.swift @@ -66,7 +66,7 @@ final class QUICStreamLoad { print( "Running QUIC stream load of \(streamCount) streams (\(concurrentStreams) at a time), with \(uploadSize) upload bytes and \(downloadSize) download bytes" ) - let startTime = NetworkClock.Instant.now + let startTime = NetworkClock.Instant.systemNow var handshakeDuration = NetworkDuration.zero var streamRoundTripDurations = [NetworkDuration]() @@ -89,7 +89,7 @@ final class QUICStreamLoad { context.activate() context.async { - let handshakeStart = NetworkClock.Instant.now + let handshakeStart = NetworkClock.Instant.systemNow // Client let clientIP = IPProtocol.instance(context: clientParameters.context) @@ -247,7 +247,7 @@ final class QUICStreamLoad { return } serverInput.start { connected in - handshakeDuration = handshakeStart.duration(to: .now) + handshakeDuration = handshakeStart.duration(to: .systemNow) group.leave() } clientInput.start() @@ -270,7 +270,7 @@ final class QUICStreamLoad { index += 1 - let streamStart = NetworkClock.Instant.now + let streamStart = NetworkClock.Instant.systemNow let myIndex = index let clientStream = StreamUpperHarness( @@ -344,7 +344,7 @@ final class QUICStreamLoad { if !clientPayloadReceived { clientStream.waitForInboundDataAvailable(completion: clientReadCompletion!) } else { - streamRoundTripDurations.append(streamStart.duration(to: .now)) + streamRoundTripDurations.append(streamStart.duration(to: .systemNow)) clientReadCompletion = nil group.leave() @@ -409,7 +409,7 @@ final class QUICStreamLoad { let meanStreamRTT = streamRoundTripDurations.reduce(NetworkDuration.zero, +) / streamRoundTripDurations.count print("Stream round trip: min = \(minStreamRTT), max = \(maxStreamRTT), mean = \(meanStreamRTT)") - let totalTime = startTime.duration(to: .now) + let totalTime = startTime.duration(to: .systemNow) let rate = (Int64(streamCount) * 1_000_000_000) / totalTime.nanoseconds print("Rate: \(rate) streams/s") diff --git a/Tests/QUICTests/CubicTests.swift b/Tests/QUICTests/CubicTests.swift index b516efc7..6ed0ea23 100644 --- a/Tests/QUICTests/CubicTests.swift +++ b/Tests/QUICTests/CubicTests.swift @@ -455,7 +455,7 @@ final class CubicTests: XCTestCase { XCTAssertEqual(path.pacer.burstSize, 10000) XCTAssertEqual(path.congestionControlWindow, 12000) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.systemNow for _ in 0..<10 { path.congestionControlPacketsSent(bytesSent: 1000) } @@ -475,7 +475,7 @@ final class CubicTests: XCTestCase { sendTimeAbsolute: &sendTimeAbsolute, sendTimeContinuous: &sendTimeContinuous ) - let currentTime = NetworkClock.Instant.now + let currentTime = NetworkClock.Instant.systemNow // First packet should be sent out almost immediately XCTAssertTrue( sendTimeAbsolute @@ -512,7 +512,7 @@ final class CubicTests: XCTestCase { // double-check that the rounding happens as expected XCTAssertEqual(rtt.smoothedRTT.microseconds, 0, "smoothedRTT is expected to round to zero microseconds") - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.systemNow path.congestionControlPacketsSent(bytesSent: 1000) path.congestionControlAckBegin() path.congestionControlPacketsAcked(bytesAcked: 1000, sentTime: time) diff --git a/Tests/QUICTests/PacerTests.swift b/Tests/QUICTests/PacerTests.swift index f1d0b6a0..ead4e26b 100644 --- a/Tests/QUICTests/PacerTests.swift +++ b/Tests/QUICTests/PacerTests.swift @@ -41,7 +41,7 @@ final class PacerTests: XCTestCase { var sendTimeAbsolute = NetworkClock.Instant(nanoseconds: 0) var sendTimeContinuous = NetworkClock.Instant(nanoseconds: 0) - let timeBefore = NetworkClock.Instant.nowAbsolute + let timeBefore = NetworkClock.Instant.systemNowAbsolute pacer.getSendTime( path: path, packetLength: packetLength, @@ -49,7 +49,7 @@ final class PacerTests: XCTestCase { sendTimeContinuous: &sendTimeContinuous ) - let timeAfter = NetworkClock.Instant.nowAbsolute + let timeAfter = NetworkClock.Instant.systemNowAbsolute XCTAssertGreaterThanOrEqual( pacer.packetSentTime, @@ -127,7 +127,7 @@ final class PacerTests: XCTestCase { sendTimeAbsolute: &sendTimeAbsolute, sendTimeContinuous: &sendTimeContinuous ) - let currentTime = NetworkClock.Instant.now + let currentTime = NetworkClock.Instant.systemNow // First packet should be sent out almost immediately XCTAssertTrue( sendTimeAbsolute <= (currentTime + NetworkClock.Instant(nanoseconds: 1_000_000).time) diff --git a/Tests/QUICTests/QLogTests.swift b/Tests/QUICTests/QLogTests.swift index ae24ce73..8236b51f 100644 --- a/Tests/QUICTests/QLogTests.swift +++ b/Tests/QUICTests/QLogTests.swift @@ -26,10 +26,10 @@ import XCTest let qlogTestsLogPrefixer = LogPrefixer("[QLogTests]") final class QLogTests: XCTestCase { - var qlog: QLog = QLog() + var qlog: QLog = QLog(context: NetworkContext(identifier: "QLogTests")) override func setUp() { - qlog = QLog() + qlog = QLog(context: NetworkContext(identifier: "QLogTests")) } func assertStringJSONContent( diff --git a/Tests/QUICTests/QUICPathValidationMessageTests.swift b/Tests/QUICTests/QUICPathValidationMessageTests.swift index 8d5d2a2b..2f47ce44 100644 --- a/Tests/QUICTests/QUICPathValidationMessageTests.swift +++ b/Tests/QUICTests/QUICPathValidationMessageTests.swift @@ -58,7 +58,7 @@ class QUICPathValidationMessageTests: XCTestCase { func testOutgoingPathChallenge() { XCTAssertEqual(path.state, .cidAssigned) - let startTime = NetworkClock.Instant.now + let startTime = NetworkClock.Instant.systemNow var pendingItems = PendingItems(packetNumberSpace: .applicationData) path.addPendingItems(&pendingItems, now: startTime) // should be empty XCTAssertTrue(pendingItems.pathChallenges.isEmpty) diff --git a/Tests/QUICTests/RecoveryTests.swift b/Tests/QUICTests/RecoveryTests.swift index 2d7ef64c..4574a164 100644 --- a/Tests/QUICTests/RecoveryTests.swift +++ b/Tests/QUICTests/RecoveryTests.swift @@ -573,7 +573,7 @@ final class RecoveryTests: XCTestCase { packet.transmittedItems.ping = true let sentPath = connection.currentPath?.identifier ?? .none packet.sentPath = sentPath - var timeNow = NetworkClock.Instant.now + var timeNow = NetworkClock.Instant.systemNow sentPacket(packet, connection: connection) XCTAssertEqual( connection.recovery.getLargestSentPN(packetNumberSpace: .initial), diff --git a/Tests/SwiftNetworkTests/SwiftNetworkClockTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkClockTests.swift index 8bee5e6f..2dd9569f 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkClockTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkClockTests.swift @@ -271,8 +271,8 @@ final class SwiftNetworkClockTests: NetTestCase { } func testInstantNowAbsoluteIsMonotonic() throws { - let first = NetworkClock.Instant.nowAbsolute - let second = NetworkClock.Instant.nowAbsolute + let first = NetworkClock.Instant.systemNowAbsolute + let second = NetworkClock.Instant.systemNowAbsolute XCTAssertNotEqual(first.time, .zero) XCTAssertTrue(second >= first) } @@ -327,123 +327,3 @@ final class SwiftNetworkClockTests: NetTestCase { } } } - -#if !NETWORK_INTERNAL_TESTS -private struct ManualClockUnavailable: LocalizedError, CustomStringConvertible { - var description: String { - "the manual clock is not compiled in; build with -Xswiftc -DNETWORK_INTERNAL_TESTS" - } - var errorDescription: String? { self.description } -} -#endif - -/// Tests for the manually advanced clock behind `NetworkClock.Instant.now`. -@available(Network 0.1.0, *) -final class SwiftNetworkManualClockTests: NetTestCase { - private let base = NetworkClock.Instant(milliseconds: 1000) - - override func setUpWithError() throws { - #if !NETWORK_INTERNAL_TESTS - throw ManualClockUnavailable() - #endif - } - - override func tearDown() { - NetworkClock.Instant.useSystemTime() - } - - func testSystemClockIsUsedByDefault() { - let first = NetworkClock.Instant.now - XCTAssertNotEqual(first, .zero) - usleep(1) - let second = NetworkClock.Instant.now - XCTAssertGreaterThan(second, first) - } - - func testUseManualTimeFreezesTheClock() { - NetworkClock.Instant.useManualTime(base) - XCTAssertEqual(NetworkClock.Instant.now, base) - // Reading repeatedly must yield the same instant: time no longer moves - // on its own, which is the entire point of the manual clock. - usleep(1) - XCTAssertEqual(NetworkClock.Instant.now, base) - XCTAssertEqual(NetworkClock.Instant.now, NetworkClock.Instant.now) - } - - func testUseManualTimeDefaultsAbsoluteToContinuous() { - NetworkClock.Instant.useManualTime(base) - XCTAssertEqual(NetworkClock.Instant.nowAbsolute, base) - } - - func testUseManualTimeKeepsContinuousAndAbsoluteSeparate() { - let absolute = NetworkClock.Instant(milliseconds: 5000) - NetworkClock.Instant.useManualTime(base, absolute: absolute) - XCTAssertEqual(NetworkClock.Instant.now, base) - XCTAssertEqual(NetworkClock.Instant.nowAbsolute, absolute) - } - - func testUseManualTimeOverwritesAPreviousManualTime() { - NetworkClock.Instant.useManualTime(base, absolute: NetworkClock.Instant(milliseconds: 5000)) - let later = NetworkClock.Instant(milliseconds: 2000) - NetworkClock.Instant.useManualTime(later) - XCTAssertEqual(NetworkClock.Instant.now, later) - XCTAssertEqual(NetworkClock.Instant.nowAbsolute, later) - } - - func testAdvanceManualTimeMovesBothClocks() { - let absolute = NetworkClock.Instant(milliseconds: 5000) - NetworkClock.Instant.useManualTime(base, absolute: absolute) - NetworkClock.Instant.advanceManualTime(by: .milliseconds(250)) - XCTAssertEqual(NetworkClock.Instant.now, base.advanced(by: .milliseconds(250))) - XCTAssertEqual(NetworkClock.Instant.nowAbsolute, absolute.advanced(by: .milliseconds(250))) - } - - func testAdvanceManualTimeAccumulates() { - NetworkClock.Instant.useManualTime(base) - for _ in 0..<3 { - NetworkClock.Instant.advanceManualTime(by: .milliseconds(100)) - } - XCTAssertEqual(NetworkClock.Instant.now, base.advanced(by: .milliseconds(300))) - } - - func testAdvanceManualTimeByZeroLeavesTheClockAlone() { - NetworkClock.Instant.useManualTime(base) - NetworkClock.Instant.advanceManualTime(by: .zero) - XCTAssertEqual(NetworkClock.Instant.now, base) - } - - func testAdvanceManualTimeKeepsNanosecondResolution() { - // `System.Time.now()` truncates to microseconds, so nanosecond steps are - // only observable on the manual clock. - NetworkClock.Instant.useManualTime(NetworkClock.Instant(nanoseconds: 1)) - NetworkClock.Instant.advanceManualTime(by: .nanoseconds(1)) - XCTAssertEqual(NetworkClock.Instant.now.time, .nanoseconds(2)) - } - - func testDurationIsMeasuredAcrossManualAdvances() { - NetworkClock.Instant.useManualTime(base) - let start = NetworkClock.Instant.now - - NetworkClock.Instant.advanceManualTime(by: .milliseconds(5)) - - // The reason the manual clock exists: an exact, reproducible elapsed - // time with no dependency on how long the test itself took to run. - XCTAssertEqual(start.duration(to: NetworkClock.Instant.now), .milliseconds(5)) - } - - func testUseSystemTimeRestoresTheSystemClock() { - NetworkClock.Instant.useManualTime(base) - XCTAssertEqual(NetworkClock.Instant.now, base) - - NetworkClock.Instant.useSystemTime() - - // `System.Time.now()` reports microseconds since boot, so the restored - // clock cannot still read the 1 s manual value, and it must keep moving. - let restored = NetworkClock.Instant.now - XCTAssertNotEqual(restored, base) - usleep(1) - let next = NetworkClock.Instant.now - XCTAssertGreaterThan(next, restored) - } - -} diff --git a/Tests/SwiftNetworkTests/SwiftNetworkContextTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkContextTests.swift index 7e53a94f..ae8d74b1 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkContextTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkContextTests.swift @@ -99,6 +99,83 @@ final class SwiftNetworkContextTests: NetTestCase { XCTAssertEqual(scheduler.unscheduledReferences, [timerReference]) } + /// The context must report the external scheduler's time, not the system's. + func testContextReportsTheExternalSchedulersTime() { + let scheduler = AdvancingScheduler() + let context = NetworkContext(identifier: "test", externalScheduler: scheduler) + + XCTAssertEqual(context.now, scheduler.now) + XCTAssertEqual(context.nowAbsolute, scheduler.nowAbsolute) + + let start = context.now + scheduler.advance(by: .milliseconds(250)) + + XCTAssertEqual(context.now, start.advanced(by: .milliseconds(250))) + } + + func testLongAdvanceDurationIsHonored() { + let scheduler = AdvancingScheduler() + let context = NetworkContext(identifier: "test", externalScheduler: scheduler) + + let start = context.now + scheduler.advance(by: .days(5)) + + XCTAssertEqual(start.duration(to: context.now), .days(5)) + } + + func testShortAdvanceIsExact() { + let scheduler = AdvancingScheduler() + let context = NetworkContext(identifier: "test", externalScheduler: scheduler) + + let start = context.now + // `System.Time.now()` divides down to microseconds, + // so a 500 ns advance is a duration it cannot represent at all + scheduler.advance(by: .nanoseconds(500)) + + XCTAssertEqual(start.duration(to: context.now), .nanoseconds(500)) + } + + func testBothClocksAdvanceTogether() { + let scheduler = AdvancingScheduler() + let context = NetworkContext(identifier: "test", externalScheduler: scheduler) + + let offsetBefore = context.now.duration(to: context.nowAbsolute) + scheduler.advance(by: .seconds(2)) + let offsetAfter = context.now.duration(to: context.nowAbsolute) + + XCTAssertEqual( + offsetAfter, + offsetBefore, + "the clocks drifted by \(offsetAfter.nanoseconds - offsetBefore.nanoseconds) ns" + ) + } + + /// A scheduler whose clock a test moves by hand. + /// **NOTE:** Arms nothing: the tests that use it assert on the time the context reports, not on anything firing. + private final class AdvancingScheduler: NetworkContext.Scheduler { + /// The two clocks start apart, so a context that reports one in place of the other fails + /// the equality check instead of matching by coincidence. + private(set) var now = NetworkClock.Instant(milliseconds: 1000) + private(set) var nowAbsolute = NetworkClock.Instant(milliseconds: 5000) + + func advance(by duration: NetworkDuration) { + now = now.advanced(by: duration) + nowAbsolute = nowAbsolute.advanced(by: duration) + } + + func runImmediate(_ task: @escaping (() -> Void)) { + task() + } + + func schedule(_ task: @escaping (() -> Void), after delay: NetworkDuration, reference: TimerReference) { + } + + func unschedule(reference: TimerReference) { + } + + var runningInScheduler: Bool { true } + } + /// Records what it was asked to schedule instead of arming anything, so a test can assert on /// the delay a caller asked for rather than on time passing. private final class RecordingScheduler: NetworkContext.Scheduler { @@ -118,6 +195,11 @@ final class SwiftNetworkContextTests: NetTestCase { } var runningInScheduler: Bool { true } + + /// A fixed instant. Nothing here fires on a deadline, so no assertion depends on the time + /// this scheduler reports. + var now: NetworkClock.Instant { NetworkClock.Instant(milliseconds: 1000) } + var nowAbsolute: NetworkClock.Instant { now } } func testContextTimerReferences() { diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICIdleTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICIdleTests.swift index dcf27af2..86cab0d0 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICIdleTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICIdleTests.swift @@ -85,7 +85,7 @@ final class SwiftNetworkQUICIdleTests: NetTestCase { ) // Once the delayed ACK has been sent there are no obligations left. - client.ack.timerFired(at: .now) + client.ack.timerFired(at: .systemNow) XCTAssertEqual( client.ack.unackedPacketCount, 0,