Skip to content
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
6 changes: 3 additions & 3 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
/usr/bin/xcodebuild -quiet -scheme swift-network-evolution-Package -destination "generic/platform=visionos" build && visionos=PASSED || { rc=1; visionos=FAILED; };
echo "=== visionOS build: $visionos ===";
echo "=== xcrun swift test ===";
xcrun swift test --quiet "$@" && tests=PASSED || { rc=1; tests=FAILED; };
xcrun swift test --quiet -Xswiftc -DNETWORK_INTERNAL_TESTS "$@" && tests=PASSED || { rc=1; tests=FAILED; };
echo "=== swift test: $tests ===";
echo "=== Summary ===";
printf "%-20s %s\n" "macOS build:" "$macos";
Expand All @@ -62,10 +62,10 @@ jobs:
swift build --build-tests --quiet --traits DisableDebugLogging,DisableErrorLogging "$@";
echo "=== Build (reductive traits on): PASSED ===";
echo "=== Test (debug) ===";
swift test --quiet "$@";
swift test --quiet -Xswiftc -DNETWORK_INTERNAL_TESTS "$@";
echo "=== Test (debug): PASSED ===";
echo "=== Test (release) ===";
swift test --quiet -c release "$@";
swift test --quiet -c release -Xswiftc -DNETWORK_INTERNAL_TESTS "$@";
echo "=== Test (release): PASSED ===";
echo "=== Summary: all builds and tests passed ==="
' bash
Expand Down
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ 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"),
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ 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
Expand Down
71 changes: 66 additions & 5 deletions Sources/SwiftNetwork/Utilities/NetworkClock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,12 @@ public struct NetworkDuration: DurationProtocol, Hashable, Equatable, CustomStri
}
}

/// A continuous clock with a compact representation and configurable initial value.
/// A continuous clock with a compact representation that tests can advance manually.
///
/// Mimics `Swift.ContinuousClock`, with two differences:
/// 1. It uses `NetworkDuration` internally so its size is 8 bytes.
/// 2. You can create a clock with any value, which is useful for unit tests.
/// 2. Tests can replace the OS clock with one they advance by hand,
/// which makes time-dependent behaviour deterministic.
#if !NETWORK_EMBEDDED
@_spi(Essentials)
// Availability due to `SwiftNetwork`'s `System.Time` (used by `Instant.now`)
Expand All @@ -224,6 +225,55 @@ 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)
}
Expand Down Expand Up @@ -265,12 +315,23 @@ public struct NetworkClock: Clock {
}

public static var now: Instant {
// TODO: this should probably call ContinuousClock.now instead
Instant(microseconds: Int64(System.Time.now()))
#if NETWORK_INTERNAL_TESTS
let manual = manualTime.continuous
if _slowPath(manual != .zero) {
return manual
}
#endif
return Instant(microseconds: Int64(System.Time.now()))
}

public static var nowAbsolute: Instant {
Instant(nanoseconds: Int64(System.Time.nowAbsoluteNanoseconds()))
#if NETWORK_INTERNAL_TESTS
let manual = manualTime.absolute
if _slowPath(manual != .zero) {
return manual
}
#endif
return Instant(nanoseconds: Int64(System.Time.nowAbsoluteNanoseconds()))
}

public static var zero: Instant {
Expand Down
120 changes: 120 additions & 0 deletions Tests/SwiftNetworkTests/SwiftNetworkClockTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,123 @@ 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)
}

}
Loading