Skip to content

Instantly share code, notes, and snippets.

@minsOne
Created August 26, 2025 19:28
Show Gist options
  • Select an option

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

Select an option

Save minsOne/91b0538ba95df9dffd0dd6306f9f3f04 to your computer and use it in GitHub Desktop.
SwiftAsyncTestPolling.swift - AsyncStream
public struct EventuallyTimeoutError: Error, CustomStringConvertible {
public let label: String
public let message: String
public var description: String { label.isEmpty ? message : "[\(label)] \(message)" }
public init(label: String = "", message: String) {
self.label = label
self.message = message
}
}
func makePollingStream<T: Sendable>(
current: @escaping @Sendable () async -> T,
until: @escaping @Sendable (T) -> Bool,
interval: Duration = .milliseconds(20),
timeout: Duration = .seconds(2),
isSame: (@Sendable (T, T) -> Bool)? = nil
) -> AsyncStream<T> {
let (stream, continuation) = AsyncStream<T>.makeStream(of: T.self)
let clock = ContinuousClock()
Task {
let start = clock.now
var last: T? = nil
var now = await current()
continuation.yield(now)
last = now
if until(now) {
continuation.finish()
return
}
while clock.now - start < timeout {
try? await clock.sleep(for: interval)
now = await current()
if let cmp = isSame, let prev = last {
if cmp(now, prev) == false {
continuation.yield(now)
last = now
}
} else {
continuation.yield(now)
last = now
}
if until(now) { break }
}
continuation.finish()
}
return stream
}
public func assertEventually<T: Sendable>(
_ label: String = "",
current: @escaping @Sendable () async -> T,
until: @escaping @Sendable (T) -> Bool,
timeout: TimeInterval = 2.0,
interval: TimeInterval = 0.02,
isSame: (@Sendable (T, T) -> Bool)? = nil,
describe: @escaping (T) -> String = { "\($0)" }
) async throws {
let stream = makePollingStream(
current: current,
until: until,
interval: .seconds(interval),
timeout: .seconds(timeout),
isSame: isSame
)
var last: T?
for await v in stream {
last = v
if until(v) { return }
}
throw EventuallyTimeoutError(
label: label,
message: "Timed out. last=\(last.map(describe) ?? "nil")"
)
}
public func assertEventuallyEqual<T: Equatable & Sendable>(
_ expected: T,
_ current: @escaping @Sendable () async -> T,
label: String = "",
timeout: TimeInterval = 2.0,
interval: TimeInterval = 0.02,
describe: @escaping (T) -> String = { "\($0)" }
) async throws {
try await assertEventually(
label,
current: current,
until: { $0 == expected },
timeout: timeout,
interval: interval,
isSame: { $0 == $1 },
describe: { "value=\(describe($0)), expected=\(expected)" }
)
}
public struct Check<T: Sendable & Equatable>: Sendable {
let name: String
let expected: T
let current: @Sendable () async -> T
let describe: @Sendable (T) -> String
public init(
name: String,
expected: T,
current: @escaping @Sendable () async -> T,
describe: @escaping @Sendable (T) -> String = { "\($0)" }
) {
self.name = name
self.current = current
self.expected = expected
self.describe = describe
}
public func isSame(_ lhs: T, _ rhs: T) -> Bool {
lhs == rhs
}
public func debugExpectedDescription() -> String {
"expected=\(describe(expected))"
}
}
struct ItemResult<T: Sendable>: Sendable {
let name: String
let last: T?
let isSatisfied: Bool
}
public func assertEventuallyAllEqual<T: Sendable>(
_ checks: [Check<T>],
label: String = "",
timeout: TimeInterval = 2.0,
interval: TimeInterval = 0.02,
) async throws {
let results: [ItemResult<T>] = try await withThrowingTaskGroup(of: ItemResult<T>.self) { group in
for check in checks {
group.addTask {
let stream = makePollingStream(
current: check.current,
until: { check.expected == $0 },
interval: .seconds(interval),
timeout: .seconds(timeout),
isSame: check.isSame(_:_:)
)
var last: T?
for await value in stream {
last = value
if check.expected == value {
return .init(name: check.name, last: value, isSatisfied: true)
}
}
return .init(name: check.name, last: last, isSatisfied: false)
}
}
var out: [ItemResult<T>] = []
for try await result in group {
out.append(result)
}
return out
}
if results.allSatisfy(\.isSatisfied) { return }
let lines = results.map { result -> String in
let check = checks.first { $0.name == result.name }
guard let check else {
return "• \(result.name): (unknown check)"
}
return result.isSatisfied
? "• \(result.name): satisfied (last=\(result.last.map(check.describe) ?? "nil"))"
: "• \(result.name): ✗ timeout (last=\(result.last.map(check.describe) ?? "nil"), \(check.debugExpectedDescription()))"
}.joined(separator: "\n")
throw EventuallyTimeoutError(
label: label,
message: "Timed out waiting for all checks.\n" + lines
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment