Skip to content

Mock clock - #191

Open
rnro wants to merge 2 commits into
apple:mainfrom
rnro:mockable-clock-test-support
Open

rnro wants to merge 2 commits into
apple:mainfrom
rnro:mockable-clock-test-support

Conversation

@rnro

@rnro rnro commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Add a test-support target for driving virtual time

ManualScheduler and ManualTimeContext let a test drive the stack's timers without reading a real clock, now that the scheduler owns time. They need a home outside the library, so nothing ships them, and outside either test target, so both can import them.

  • Added run(until:) analogous to XCTest's wait(for:timeout:), reporting whether the condition held rather than blocking on real time.
  • Mocking fires each timer with now set to its own deadline, so a timer that reads the clock sees the instant it was scheduled for rather than the end of the advance.
  • Moved NetworkClock.Instant.testBase into the new target and deleted Tests/QUICTests/TestClock.swift, which held nothing else.
  • Imported the new target in the five suites that use testBase.
  • Added ManualSchedulerTests to cover the scheduler.

NOTE: this is not yet adopted, that comes in a follow-up PR

A call site wanting parameters on a particular context had to declare them `var` and reassign
`context` on the next line, whether or not it went on to change anything else.
`Parameters.context` is already public, so this adds no surface beyond the initializer itself.

* Added `Parameters.init(context:)` outside the platform conditionals, so the private and embedded
  builds have it as well as the open-source one.
* Adopted it at 15 call sites, dropping the `context` reassignment from each.
* Switched ten of those sites to `let`, since they no longer mutate the value.
@rnro rnro changed the title Mockable clock Mock clock Oct 1, 2026
@rnro rnro added the 🔨 semver/patch No public API change. label Oct 1, 2026
@rnro
rnro force-pushed the mockable-clock-test-support branch from 42c686f to 91066e0 Compare October 1, 2026 13:57
Comment thread Package.swift
swiftSettings: availabilityMacros + settings
),
.target(
name: "SwiftNetworkTestSupport",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we put the support functionality into SwiftNetworkTestHarness? Happy to rename that module, but it seems pretty much the same overall purpose

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they have a different purpose, and I had actually omitted something initially which made that unclear. I think that the new target should be a product, so that external adopters can also use mock time in their tests if needed.

@agnosticdev agnosticdev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which cases will we adopt this? Will it be strictly in our unit tests or will it be in QUIC stack tests as well? The reason I ask is that for the tests that are exercising loss recovery scenarios it would be good to still use real time for those cases.

`ManualScheduler` and `ManualTimeContext` let a test drive the stack's
timers without reading a real clock, now that the scheduler owns time.
They sit outside the library so nothing in it depends on them, and are
offered as their own product so an adopter can drive time in tests of
their own code.

* Added `run(until:)` analogous to XCTest's `wait(for:timeout:)`,
  reporting whether the condition held rather than blocking on real
  time.
* Mocking fires each timer with `now` set to its own deadline, so a
  timer that reads the clock sees the instant it was scheduled for
  rather than the end of the advance.
* Moved `NetworkClock.Instant.testBase` into the new product and deleted
  `Tests/QUICTests/TestClock.swift`, which held nothing else.
* Imported the new product in the five suites that use `testBase`.
* Added `ManualSchedulerTests` to cover the scheduler.
@rnro
rnro force-pushed the mockable-clock-test-support branch from e7c10cc to 03cb069 Compare October 2, 2026 15:10
@rnro

rnro commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

In which cases will we adopt this? Will it be strictly in our unit tests or will it be in QUIC stack tests as well? The reason I ask is that for the tests that are exercising loss recovery scenarios it would be good to still use real time for those cases.

So this PR only converts the unit tests that build a context and never wait on anything. My intention is to move the QUIC stack tests over in follow-ups, loss recovery included.

The intention is that everyone agrees on one source of truth for time, so that to any code reading the clock through the context, advancing it by hand is indistinguishable from the wall clock advancing. I'd like all our tests on mocked time eventually, with two exceptions: anything driving real sockets, and the handful that exist to exercise the real dispatch timer — SwiftNetworkContextTests's timer tests have to keep a real context, since that's the thing under test.

On loss recovery specifically: nothing in those tests is real already. The transport is an in-memory bridge, loss comes from clientDrops, delay from linkDelay. Recovery.swift reads time only through connection.now - there's no system clock read anywhere under Sources/SwiftNetwork/QUIC/ - so RTT and RTO see identical inputs either way; mocking only makes them reproducible. If anything these should benefit most for reliability.

Do you see any issues with this plan which I'm missing?

@agnosticdev

Copy link
Copy Markdown
Collaborator

On loss recovery specifically: nothing in those tests is real already. The transport is an in-memory bridge, loss comes from clientDrops, delay from linkDelay. Recovery.swift reads time only through connection.now - there's no system clock read anywhere under Sources/SwiftNetwork/QUIC/ - so RTT and RTO see identical inputs either way; mocking only makes them reproducible. If anything these should benefit most for reliability.

Do you see any issues with this plan which I'm missing?

My concern is really the Recovery and Ack timers firing to run actions such as delayed ack, PTO, etc..
If we mock all of that, we rely on our consumers to test actual real timers then, correct?

@rpaulo

rpaulo commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

The intention is that everyone agrees on one source of truth for time, so that to any code reading the clock through the context, advancing it by hand is indistinguishable from the wall clock advancing. I'd like all our tests on mocked time eventually, with two exceptions: anything driving real sockets, and the handful that exist to exercise the real dispatch timer — SwiftNetworkContextTests's timer tests have to keep a real context, since that's the thing under test.

Do you see any issues with this plan which I'm missing?

I agree with this approach. It would be nice though if we could have an option to do an integration test with the real clock. We wouldn't use it by default, but we could use it when we are tracking bugs and want to run a test with real time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants