Last active
August 25, 2025 17:17
-
-
Save minsOne/f30cc00217bd88edd6d6dd2716876afb to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| public enum EventuallyError<T: Equatable & Sendable>: | |
| Error, | |
| CustomStringConvertible | |
| { | |
| case timeout(last: T, expected: T) | |
| public var description: String { | |
| switch self { | |
| case let .timeout(last, expected): | |
| return "Timed out. last=\(last), expected=\(expected)" | |
| } | |
| } | |
| } | |
| public enum EventuallyErrorAll: Error, CustomStringConvertible { | |
| case timeoutAll(String) | |
| public var description: String { | |
| switch self { | |
| case let .timeoutAll(message): message | |
| } | |
| } | |
| } | |
| public func assertEventuallyEqual<T: Equatable & Sendable>( | |
| _ expected: T, | |
| _ current: @escaping @Sendable () async -> T, | |
| timeout: TimeInterval = 2.0, | |
| interval: TimeInterval = 0.02, | |
| file: StaticString = #filePath, | |
| line: UInt = #line | |
| ) async throws { | |
| let deadline = Date().addingTimeInterval(timeout) | |
| while Date() < deadline { | |
| let value = await current() | |
| if value == expected { | |
| return | |
| } | |
| try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) | |
| } | |
| let last = await current() | |
| throw EventuallyError.timeout(last: last, expected: expected) | |
| } | |
| public struct Check<T: Equatable & Sendable>: Sendable { | |
| public let name: String | |
| public let expected: T | |
| public let current: @Sendable () async -> T | |
| public init( | |
| name: String, | |
| expected: T, | |
| current: @escaping @Sendable () async -> T | |
| ) { | |
| self.name = name | |
| self.expected = expected | |
| self.current = current | |
| } | |
| } | |
| public func assertEventuallyAllEqual<T: Equatable & Sendable>( | |
| _ checks: [Check<T>], | |
| timeout: TimeInterval = 2.0, | |
| interval: TimeInterval = 0.02, | |
| file: StaticString = #filePath, | |
| line: UInt = #line | |
| ) async throws { | |
| let deadline = Date().addingTimeInterval(timeout) | |
| while Date() < deadline { | |
| let currents: [T] = await withTaskGroup(of: T.self) { group in | |
| for c in checks { | |
| group.addTask { await c.current() } | |
| } | |
| return await group.reduce(into: []) { $0.append($1) } | |
| } | |
| let allMatch = zip(checks, currents).allSatisfy { $0.expected == $1 } | |
| if allMatch { return } | |
| try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) | |
| } | |
| let results: [(String, T, T)] = await withTaskGroup(of: (String, T, T).self) { group in | |
| for c in checks { | |
| group.addTask { | |
| let v = await c.current() | |
| return (c.name, v, c.expected) | |
| } | |
| } | |
| return await group.reduce(into: []) { $0.append($1) } | |
| } | |
| let diff = results | |
| .filter { $0.1 != $0.2 } | |
| .map { "• \($0.0): last=\($0.1) expected=\($0.2)" } | |
| .joined(separator: "\n") | |
| throw EventuallyErrorAll.timeoutAll("Timed out waiting for all equal.\n\(diff)\nfile: \(file), line: \(line)") | |
| } |
This file contains hidden or 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
| public extension XCTestCase { | |
| func assertEventuallyEqual<T: Equatable>( | |
| _ expected: T, | |
| _ current: @escaping @Sendable () async -> T, | |
| timeout: TimeInterval = 2.0, | |
| interval: TimeInterval = 0.02, | |
| file: StaticString = #filePath, | |
| line: UInt = #line | |
| ) async { | |
| let deadline = Date().addingTimeInterval(timeout) | |
| while Date() < deadline { | |
| let value = await current() | |
| if value == expected { | |
| return | |
| } | |
| try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) | |
| } | |
| let last = await current() | |
| XCTFail("Timed out. last=\(last), expected=\(expected)", file: file, line: line) | |
| } | |
| } | |
| public extension XCTestCase { | |
| func assertEventuallyAllEqual<T: Equatable>( | |
| _ checks: [Check<T>], | |
| timeout: TimeInterval = 2.0, | |
| interval: TimeInterval = 0.02, | |
| file: StaticString = #filePath, | |
| line: UInt = #line | |
| ) async { | |
| let deadline = Date().addingTimeInterval(timeout) | |
| while Date() < deadline { | |
| let currents: [T] = await withTaskGroup(of: T.self) { group in | |
| for c in checks { | |
| group.addTask { await c.current() } | |
| } | |
| return await group.reduce(into: []) { $0.append($1) } | |
| } | |
| let allMatch = zip(checks, currents).allSatisfy { $0.expected == $1 } | |
| if allMatch { return } | |
| try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) | |
| } | |
| let results: [(String, T, T)] = await withTaskGroup(of: (String, T, T).self) { group in | |
| for c in checks { | |
| group.addTask { | |
| let v = await c.current() | |
| return (c.name, v, c.expected) | |
| } | |
| } | |
| return await group.reduce(into: []) { $0.append($1) } | |
| } | |
| let diff = results | |
| .filter { $0.1 != $0.2 } | |
| .map { "• \($0.0): last=\($0.1) expected=\($0.2)" } | |
| .joined(separator: "\n") | |
| XCTFail("Timed out waiting for all equal.\n\(diff)", file: file, line: line) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment