-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
equatable conformances are for tests, but not sure if right thing to do note that I've had ot make RoomLifecycle.current async, this seems odd but not sure there's a better way if we want to make use of swift's built-in concurrency, also it highlights to callers that there can't be a definitive concept of 'current' which was indeed a concern i'd highlighted earlier but also had to make `onChange` async so that I could do some state management, that seems odder. is there a way to instead bake the async-ness into the sequence? things where testing framework makes things messy with concurrency: - `async let` with things like XCTUnwrap, XCAssertEqual - async operations with XCTAssertThrowsError I hope that once Xcode 16 is released we can instead use Swift Testing.
- Loading branch information
1 parent
721f10c
commit 6dd74bb
Showing
6 changed files
with
401 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import Ably | ||
|
||
// This file contains extensions to ably-cocoa’s types, to make them easier to use in Swift concurrency. | ||
|
||
internal extension ARTRealtimeChannelProtocol { | ||
// TODO: it's not good that this isn't automatically bridged by Swift — create an ably-cocoa issue for revisiting this developer experience | ||
func attachAsync() async throws { | ||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Swift.Error>) in | ||
attach { error in | ||
if let error { | ||
continuation.resume(throwing: error) | ||
} else { | ||
continuation.resume() | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import Ably | ||
@testable import AblyChat | ||
import XCTest | ||
|
||
final class DefaultRoomStatusTests: XCTestCase { | ||
// CHA-RS2a (TODO what's the best way to test this), CHA-RS3 | ||
func testInitialStatus() async { | ||
let status = DefaultRoomStatus() | ||
|
||
let current = await status.current | ||
XCTAssertEqual(current, .initialized) | ||
} | ||
|
||
// CHA-RL1a | ||
func testAttachWhenAlreadyAttached() async throws { | ||
let status = DefaultRoomStatus(forTestingWhatHappensWhenCurrentlyIn: .attached) | ||
|
||
// TODO: is this the right place for this to go? | ||
try await status.performAttachOperation() | ||
// TODO: how to check it's a no-op? what are the things that it might do? | ||
} | ||
|
||
// TODO: why is some tool removing this `await` below? | ||
|
||
// CHA-RL1b | ||
func testAttachWhenReleasing() async throws { | ||
let status = DefaultRoomStatus(forTestingWhatHappensWhenCurrentlyIn: .releasing) | ||
|
||
await assertThrowsARTErrorInfo(withCode: 102_102) { | ||
try await status.performAttachOperation() | ||
} | ||
} | ||
|
||
// CHA-RL1c | ||
func testAttachWhenReleased() async throws { | ||
let status = DefaultRoomStatus(forTestingWhatHappensWhenCurrentlyIn: .released) | ||
|
||
await assertThrowsARTErrorInfo(withCode: 102_103) { | ||
try await status.performAttachOperation() | ||
} | ||
} | ||
|
||
// CHA-RL1e | ||
func testAttachTransitionsToAttaching() async throws { | ||
let (continuation, attachResult) = makeAsyncFunctionWithContinuation(of: Void.self, throwing: ARTErrorInfo.self) | ||
|
||
let status = DefaultRoomStatus(contributors: [MockRealtimeChannel(attachResult: .fromFunction(attachResult))]) | ||
let statusChangeSubscription = await status.onChange(bufferingPolicy: .unbounded) | ||
async let statusChange = statusChangeSubscription.first { _ in true } | ||
|
||
async let attachSignal: Void = try await status.performAttachOperation() | ||
|
||
// Check that status change was emitted and that its `current` is as expected | ||
guard let statusChange = await statusChange else { | ||
XCTFail("Expected status change but didn’t get one") | ||
return | ||
} | ||
|
||
// Check that current status is as expected | ||
let current = await status.current | ||
XCTAssertEqual(current, .attaching) | ||
|
||
// Now that we’ve seen the ATTACHING state, allow the contributor `attach` call to complete | ||
continuation.yield(()) | ||
_ = try await attachSignal | ||
|
||
XCTAssertEqual(statusChange.current, .attaching) | ||
} | ||
|
||
// CHA-RL1g | ||
func testWhenAllContributorsAttachSuccessfullyItTransitionsToAttached() async throws { | ||
let contributors = (1 ... 3).map { _ in MockRealtimeChannel(attachResult: .complete(.success(()))) } | ||
let status = DefaultRoomStatus(contributors: contributors) | ||
|
||
let statusChangeSubscription = await status.onChange(bufferingPolicy: .unbounded) | ||
async let attachedStatusChange = statusChangeSubscription.first { $0.current == .attached } | ||
|
||
try await status.performAttachOperation() | ||
|
||
guard let statusChange = await attachedStatusChange else { | ||
XCTFail("Expected status change to attached but didn't get one") | ||
return | ||
} | ||
|
||
XCTAssertEqual(statusChange.current, .attached) | ||
|
||
// Check that current status is as expected | ||
let current = await status.current | ||
XCTAssertEqual(current, .attached) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import Ably | ||
import XCTest | ||
|
||
/** | ||
Asserts that a given async expression throws an ``ARTErrorInfo`` with a given ``ARTErrorInfo.code``. | ||
*/ | ||
// TODO: we can’t use ARTErrorCode, I suppose? That's an issue | ||
// TODO: it doesn't take an autoclosure because for whatever reason one of our linting tools removes the `await` on the expression | ||
func assertThrowsARTErrorInfo<T>(withCode expectedCode: Int, _ expression: () async throws -> T, file: StaticString = #filePath, line: UInt = #line) async { | ||
let result: Result<T, Error> | ||
|
||
do { | ||
result = try await .success(expression()) | ||
} catch { | ||
result = .failure(error) | ||
} | ||
|
||
guard case let .failure(error) = result else { | ||
XCTFail("Expected expression to throw an error", file: file, line: line) | ||
return | ||
} | ||
|
||
guard let artError = error as? ARTErrorInfo else { | ||
XCTFail("Expected expression to throw an ARTErrorInfo but got \(type(of: error))", file: file, line: line) | ||
return | ||
} | ||
|
||
XCTAssertEqual(artError.code, expectedCode, "Expected thrown ARTErrorInfo to have code \(expectedCode) but got \(artError.code)", file: file, line: line) | ||
} | ||
|
||
// TODO: note that this function can't be called multiple times | ||
// TODO: improve this API (shouldn’t return that Continuation) | ||
func makeAsyncFunctionWithContinuation<Element, Failure: Error>(of _: Element.Type = Element.self, throwing _: Failure.Type = Failure.self) -> (continuation: AsyncThrowingStream<Element, Error>.Continuation, function: () async -> Result<Element, Failure>) { | ||
let (stream, continuation) = AsyncThrowingStream.makeStream(of: Element.self) | ||
return (continuation: continuation, function: { | ||
do { | ||
return try await .success(stream.first { _ in true }!) | ||
} catch { | ||
return .failure(error as! Failure) | ||
} | ||
}) | ||
} |
Oops, something went wrong.