[AIT-846] Time mocking and Universal Test Suite ground work#2210
[AIT-846] Time mocking and Universal Test Suite ground work#2210maratal wants to merge 6 commits into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
04deb80 to
0713756
Compare
Squash of the ARTTimeProvider groundwork: a unified abstraction over the wall clock, the continuous clock, and scheduling, injected via ARTClientOptions.testOptions, with ARTSystemTimeProvider as the default. All internal wall-clock reads, scheduler calls, and the event-emitter/auth/log/encoder timestamps are routed through it so the Universal Test Suite can install a fake clock. The continuous-clock instant and the scheduler handle are expressed as two small boundary protocols defined locally in ably-cocoa: ARTContinuousClockInstantProtocol and ARTSchedulerHandle. Defining them here, rather than depending on an as-yet-unreleased version of ably-cocoa-plugin-support, keeps Package.swift at from: "2.0.0" and means ARTTimeProvider.h, ARTContinuousClock.h, and ARTGCD.h no longer @import _AblyPluginSupportPrivate, which also unblocks the non-SPM (CocoaPods / Xcode-framework) builds. ARTPluginAPI's three time forwarders are retained; they are inert until a plugin-support release exposes the matching protocol methods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Harness (Test/UTS/Harness): - MockWebSocket: a real ARTWebSocketTransport over a faked ARTWebSocket, so URL/query-param building stays real; the provider drives the simulated server (respondWithSuccess/sendToClient/simulateDisconnect). - MockHTTP: fake ARTHTTPExecutor to intercept/observe requests and respond. - FakeTimeProvider: deterministic clock, opt-in via enableFakeTimers(). - UTSTestCase: installMock / makeRealtime / makeRest, AWAIT_STATE polling, CapturingLog, NoOpReachability. Tests: ConnectionRecoveryTests (RTN16g/g1, g2, f, f1, j, k) and TimeTests (RSC16, 0-4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cocoa equivalent of ably-java's .claude/skills/uts-to-kotlin/SKILL.md (PR #1209). Invocable as `/uts-to-swift <spec-file>`, it walks the translation from a UTS pseudocode spec to a Swift test in the UTS target, mapped onto this repo's harness: installMock + makeRealtime/ makeRest, MockWebSocketProvider/MockWebSocket, MockHTTP, the ARTProtocolMessage +UTS factories, awaitConnectionState/awaitChannelState/ poll, and enableFakeTimers/advanceTime. Encodes the conventions established for this suite: the local-array capture pattern (vs. a mock property), seconds-not-wire-milliseconds for connectionStateTtl/maxIdleInterval, CapturingLog for log assertions, the test_<SPEC>_<desc> + `// UTS:` naming, RUN_DEVIATIONS env-gated skips, and the distinction between an SDK deviation (deviations.md) and a harness-driving choice (code comment). Requires that spec comments are copied verbatim and every ASSERT is either translated or annotated when it has no Swift equivalent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0713756 to
1d0e282
Compare
Convert the UTS target from XCTest to Swift Testing (`import Testing`): - UTSTestCase becomes a plain base class (no longer XCTestCase); suites are `@Suite(.serialized) final class ...: UTSTestCase`. Swift Testing creates a fresh instance per `@Test`, so teardown moves from `tearDown()` to `deinit`. - Assertions: XCTAssert* -> #expect, XCTUnwrap -> #require, XCTFail -> Issue.record (with SourceLocation), file/line params -> #_sourceLocation. - TimeTests' async REST calls drop XCTestExpectation for `async` tests that bridge the completion handler via withCheckedContinuation. Also updates the /uts-to-swift skill, README, and deviations.md to document the Swift Testing conventions (#expect/#require, @Test/@suite, the .enabled(if:) env-gated deviation skip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WebSocket mock — conformance against
|
| Spec | Ours |
|---|---|
mock_ws (test handle) |
MockWebSocketProvider |
mock_ws.active_connection / PendingConnection |
MockWebSocket (per-connection, conforms to ARTWebSocket) |
install_mock |
testOptions.transportFactory = MockWebSocketTransportFactory |
Clean mapping, and it exceeds the spec in one respect: we drive a real ARTWebSocketTransport, so URL/query-param construction (recover, resume, format) is exercised by production code, not faked.
What conforms ✅
- Message injection:
send_to_client→sendToClient,send_to_client_and_close→sendToClientAndClose,simulate_disconnect→simulateDisconnect. - Connection outcomes:
respond_with_success→respondWithSuccess(),respond_with_refused→respondWithRefused(). - Capturing client frames: spec's
MESSAGE_FROM_CLIENT→sentMessages(decoded, ordered). - Connection-closing semantics table: DISCONNECTED/connection-ERROR →
sendToClientAndClose, channel-ERROR/CONNECTED/HEARTBEAT/ACK →sendToClient, transport failure →simulateDisconnect— matches the spec table exactly. respondWithSuccess()ordering (spec §"respondWithSuccess() Ordering"): connection-open is delivered before the CONNECTED message because both go through the transport's serialdelegateDispatchQueuein enqueue order.- Test isolation, fake timers, "no arbitrary real-time delays": fresh provider per
@Test+deinitcleanup;FakeTimeProvider/advanceTime;awaitConnectionStateis anAWAIT_STATEpoll, not a fixed delay. active_connection→provider.activeConnection.
Intentional divergences (functionally equivalent) 🟡
respond_with_success(connected_message)is one call in the spec; we split it intorespondWithSuccess()+sendToClient(.connected(...)). Ordering still holds via the serial queue.- No
eventslist /await_*futures for the common case — we use the spec's other sanctioned patterns:onConnectionAttempthandler + local capture arrays, andsentMessagesfor client frames. respond_with_error(msg, then_close)— no named method, but covered bysendToClient(.error(...))(then_close=false) /sendToClientAndClose(.error(...))(then_close=true).
Gaps / not yet implemented ❌ (none block the current 6 tests)
Material (behavioral):
close()is a silent no-op (MockWebSocket.swift:104) — it only setsreadyState. The spec (§"Mock close() Must Be Asynchronous" + "Client-Initiated Close") requires it to asynchronously fireonCloseand record aCLIENT_CLOSEevent. Current tests drive close via a serverCLOSED(sendToClientAndClose), so they pass — but client-initiated-close scenarios (heartbeat-timeout, fatal-error, explicit close with no server reply) can't be tested, andawait_client_close()/CLIENT_CLOSEdon't exist.
Capability gaps:
2. No events unified timeline (MockEvent/MockEventType) → the spec's CONTAINS_IN_ORDER state/event-sequence verification isn't supported.
3. await_next_message_from_client / await_connection_attempt / await_client_close — the "await pattern" for advanced sequential coordination (inspect-then-decide). We only have the handler+counter pattern.
4. send_ping_frame() (RTN23b) — missing, and likely needs an ARTWebSocket delegate hook; SocketRocket may not surface pings, so this may be partly platform-N/A (to confirm).
5. WS-level respond_with_timeout() / respond_with_dns_error() — present on MockHTTP but not on MockWebSocket (we only have respondWithRefused). Needed for RTN17/RTN14 network-failure tests.
6. Raw-frame hooks onTextDataFrame / onBinaryDataFrame (and onMessageFromClient) — we decode straight to sentMessages, so wire-encoding assertions ("null fields omitted") aren't possible.
7. simulate_disconnect(error?) ignores the optional ErrorInfo; reset() absent (we use fresh instances); PendingConnection.protocol/timestamp not exposed.
8. Protocol templates: we have .connected/.attached/.error/.ack/.closed; missing .disconnected and .heartbeat factories.
Bottom line
For the RTN16 connection-recovery surface implemented here, the mock is faithful — closing semantics, success-ordering, isolation, and fake-timer rules all match. The gaps are all in areas no current test exercises. Highest-value items to close next, in order: (1) async close() + CLIENT_CLOSE/await_client_close (unlocks close/heartbeat tests), (5) WS timeout/DNS (network-failure tests), then (2) the events timeline (sequence assertions).
There was a problem hiding this comment.
Pull request overview
This PR lays the groundwork for running Ably’s Universal Test Suite (UTS) against ably-cocoa by introducing an injectable ARTTimeProvider abstraction (covering wall clock, continuous clock, and scheduling) and by adding a new standalone UTS Swift Testing target with a mock transport/HTTP/time harness and initial derived tests.
Changes:
- Introduce
ARTTimeProvider+ defaultARTSystemTimeProvider, and route internal timestamps/timers/scheduling through it. - Add a new
UTSSwift Testing target (Test/UTS) with a deterministic fake clock, mocked WebSocket/HTTP, and initial RTN16/RSC16 derived tests. - Update internal/private headers, module maps, and a few existing tests to align with the new time/scheduler APIs.
Reviewed changes
Copilot reviewed 48 out of 48 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Test/UTS/Tests/TimeTests.swift | Adds RSC16 time() derived tests using mocked HTTP. |
| Test/UTS/Tests/ConnectionRecoveryTests.swift | Adds RTN16 recovery derived tests using mocked WebSocket and fake timers where needed. |
| Test/UTS/README.md | Documents UTS target layout, harness primitives, and running instructions. |
| Test/UTS/Harness/UTSTestCase.swift | Provides shared harness utilities: installMock/makeRealtime/makeRest, polling, fake-timer control, cleanup. |
| Test/UTS/Harness/NoOpReachability.swift | Disables OS reachability monitoring during unit tests. |
| Test/UTS/Harness/MockWebSocket.swift | Implements a fake ARTWebSocket + provider/factory to exercise real transport URL/query construction. |
| Test/UTS/Harness/MockHTTP.swift | Implements a fake ARTHTTPExecutor to intercept requests and inject responses without network I/O. |
| Test/UTS/Harness/FakeTimeProvider.swift | Implements deterministic ARTTimeProvider for controllable clocks and scheduling, plus timer leak safety net. |
| Test/UTS/Harness/CapturingLog.swift | Adds an ARTLog subclass to capture log output for assertions. |
| Test/UTS/Harness/ARTProtocolMessage+UTS.swift | Adds protocol-message factory helpers for UTS tests. |
| Test/UTS/deviations.md | Adds a template for documenting spec deviations and env-gated deviation tests. |
| Test/AblyTests/Tests/UtilitiesTests.swift | Updates ARTInternalEventEmitter init sites to pass a time provider. |
| Test/AblyTests/Tests/GCDTests.swift | Updates scheduled-block handle construction to the new ARTScheduledBlockHandle API. |
| Test/AblyTests/Tests/ContinuousClockTests.swift | Removes continuous clock tests that depended on the removed ARTContinuousClock class API. |
| Test/Ably.xctestplan | Adds the UTS target to the Xcode test plan. |
| Source/PrivateHeaders/Ably/ARTTimeProvider.h | Introduces the unified time abstraction protocol used internally. |
| Source/PrivateHeaders/Ably/ARTTestClientOptions.h | Adds timeProvider to test options for injection. |
| Source/PrivateHeaders/Ably/ARTSystemTimeProvider.h | Declares the default system-backed time provider. |
| Source/PrivateHeaders/Ably/ARTSchedulerHandle.h | Introduces a cancellable scheduler handle protocol. |
| Source/PrivateHeaders/Ably/ARTRest+Private.h | Exposes timeProvider and updates fallback expiration to protocol-based continuous instants. |
| Source/PrivateHeaders/Ably/ARTRealtime+Private.h | Exposes timeProvider from realtime internals for plugins and internal use. |
| Source/PrivateHeaders/Ably/ARTLog+Private.h | Adds injectable time provider for log timestamping. |
| Source/PrivateHeaders/Ably/ARTGCD.h | Updates scheduled-block handle to conform to ARTSchedulerHandle; removes old helper functions. |
| Source/PrivateHeaders/Ably/ARTEventEmitter+Private.h | Threads timeProvider through event emitter internals and updates initializers accordingly. |
| Source/PrivateHeaders/Ably/ARTContinuousClockInstantProtocol.h | Introduces protocol for continuous-clock instants (type-erased across time providers). |
| Source/PrivateHeaders/Ably/ARTContinuousClock.h | Refactors to expose only the ARTContinuousClockInstant concrete instant type conforming to the protocol. |
| Source/include/module.modulemap | Exposes new private headers via the module map. |
| Source/ARTWebSocketTransport.m | Injects timeProvider into the transport state emitter. |
| Source/ARTTestClientOptions.m | Initializes and copies the default ARTSystemTimeProvider in test options. |
| Source/ARTSystemTimeProvider.m | Implements the system-backed wall clock, continuous clock, and scheduler. |
| Source/ARTRest.m | Replaces direct continuous-clock usage with injected timeProvider. |
| Source/ARTRealtimePresence.m | Routes presence timestamps and event emitters through injected timeProvider. |
| Source/ARTRealtimeChannel.m | Passes timeProvider into internal channel event emitters. |
| Source/ARTRealtimeAnnotations.m | Passes timeProvider into annotations event emitter. |
| Source/ARTRealtime.m | Replaces [NSDate date]/dispatch scheduling with timeProvider wall clock + scheduler handles. |
| Source/ARTPluginAPI.m | Exposes time primitives/scheduling to plugins through the plugin API. |
| Source/ARTLog.m | Uses injected time provider for log line timestamps (defaulting to system provider). |
| Source/ARTJsonLikeEncoder.m | Uses injected wall clock for token-request timestamps instead of [NSDate date]. |
| Source/ARTGCD.m | Removes the old artDispatchScheduled helper implementation. |
| Source/ARTEventEmitter.m | Replaces dispatch scheduling with timeProvider scheduling; threads provider through emitter initializers. |
| Source/ARTContinuousClock.m | Refactors continuous instant comparison/addition to protocol-based API. |
| Source/ARTAuth.m | Uses injected wall clock for auth’s notion of current time and for cancellation event emitter scheduling. |
| Source/Ably.modulemap | Exposes new private headers via the framework module map. |
| Package.swift | Adds the UTS SwiftPM test target. |
| CONTRIBUTING.md | Documents the new “Time-related operations” convention using ARTTimeProvider. |
| Ably.xcodeproj/project.pbxproj | Adds new headers/sources to Xcode project and header build phases. |
| .swiftpm/xcode/xcshareddata/xcschemes/ably-cocoa.xcscheme | Adds UTS to the SwiftPM-generated Xcode scheme test action. |
| .claude/skills/uts-to-swift/SKILL.md | Adds a Claude Code skill spec for translating UTS pseudocode specs into Swift tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Addresses review feedback (PR #2209): the local captured-array pattern (`var captured = []` appended from a mock handler) is a data race the Swift 6 compiler rejects, and the suite should use the latest language mode so such races are caught. - Package.swift: build the UTS target with `-swift-version 6`. - Import Ably / Ably.Private `@preconcurrency` in the harness, since the Objective-C SDK isn't Sendable-audited; this keeps strict checking for our own code while treating SDK interop as warnings. - Make the mocks Sendable with their existing internal synchronisation: MockWebSocketProvider / MockWebSocket / MockHTTP / PendingHTTP* / FakeTimeProvider are `@unchecked Sendable`, and their handler closures are `@Sendable`. - Add `Captured<T>`, a lock-guarded, Sendable collector for the spec's local `captured_*` arrays; tests use it instead of a mutable `var` array (which now fails to compile inside a `@Sendable` handler). The "first attempt vs later" branches switch on `Captured.count` rather than a separate mutable counter. - Update the /uts-to-swift skill and README accordingly. All 11 tests pass; the suite builds warning-clean under Swift 6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ARTContinuousClock.m: -isAfter: now guards the downcast of the id<ARTContinuousClockInstantProtocol> argument and raises a clear exception on a type mismatch, instead of force-casting and risking an invalid memory read. - CONTRIBUTING.md: the "Time-related operations" bullets referenced the old APContinuousClockInstant / APSchedulerHandle names; point them at the actual ARTContinuousClockInstantProtocol / ARTSchedulerHandle. - Package.swift: the UTS target comment said "XCTest suite"; it's a Swift Testing suite. - MockHTTP: execute(_:completion:) now fails fast (assertion + error callback) when no onRequest handler is installed, rather than leaving the request (and any awaiting test) hanging with the callback uncalled. The README layout drift Copilot also flagged was already corrected in the preceding commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Could
|
Supersedes #2209 (rebased onto a clean base with the plugin-support dependency change dropped).
What's here
Mock for time — squash of the
ARTTimeProvidergroundwork: a unified abstraction over the wall clock, the continuous clock, and scheduling, injected viaARTClientOptions.testOptions, withARTSystemTimeProvideras the default. All internal wall-clock reads, scheduler calls, and the event-emitter/auth/log/encoder timestamps are routed through it so the Universal Test Suite can install a fake clock.Add Universal Test Suite (UTS) target — a standalone XCTest suite (
Test/UTS) derived from the language-neutral specs inably/specification(uts/). Mocked WebSocket (realARTWebSocketTransportover a fakedARTWebSocket), mocked HTTP, a deterministicFakeTimeProvider, andUTSTestCaseharness. Tests:ConnectionRecoveryTests(RTN16) andTimeTests(RSC16).Add
/uts-to-swifttranslation skill — a Claude Code skill that translates a UTS pseudocode spec into a Swift test against this harness (the cocoa equivalent of ably-java'suts-to-kotlin).🤖 Generated with Claude Code