Skip to content

Instantly share code, notes, and snippets.

@minsOne
Last active August 25, 2025 17:17
Show Gist options
  • Select an option

  • Save minsOne/f30cc00217bd88edd6d6dd2716876afb to your computer and use it in GitHub Desktop.

Select an option

Save minsOne/f30cc00217bd88edd6d6dd2716876afb to your computer and use it in GitHub Desktop.
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)")
}
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