forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAwaitWithTimeoutTests.swift
More file actions
77 lines (70 loc) · 3.51 KB
/
Copy pathAwaitWithTimeoutTests.swift
File metadata and controls
77 lines (70 loc) · 3.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import XCTest
@testable import Omi_Computer
/// Ratchet for the automation-bridge `/state` hang fix: `awaitWithTimeout` must
/// bound the live MainActor refresh so a wedged main thread (e.g. a blocking
/// Keychain read during sign-in) can't hang the bridge. The load-bearing property
/// is that it returns at the timeout WITHOUT waiting for a non-cancellable, still-
/// running operation — a `withTaskGroup` implementation would deadlock here
/// because it awaits all child tasks at scope exit.
final class AwaitWithTimeoutTests: XCTestCase {
func testReturnsOperationValueWhenItFinishesFirst() async {
let result = await awaitWithTimeout(.seconds(5)) { "live" }
XCTAssertEqual(result, "live")
}
func testReturnsNilWhenOperationExceedsTimeout() async {
let start = Date()
let result = await awaitWithTimeout(.milliseconds(50)) { () -> String in
try? await Task.sleep(for: .seconds(10))
return "late"
}
let elapsed = Date().timeIntervalSince(start)
XCTAssertNil(result)
XCTAssertLessThan(elapsed, 5.0, "must not wait out the full operation")
}
/// The regression guard. The operation blocks a background thread on a semaphore
/// the test releases only AFTER asserting — so the operation is genuinely still
/// running and non-cancellable when the timeout fires. `awaitWithTimeout` must
/// still return promptly; a task-group version would hang until `gate.signal()`.
func testReturnsAtTimeoutEvenWhenOperationIsBlockedAndNonCancellable() async {
let gate = DispatchSemaphore(value: 0)
let start = Date()
let result = await awaitWithTimeout(.milliseconds(100)) { () -> String in
await withCheckedContinuation { (continuation: CheckedContinuation<String, Never>) in
DispatchQueue.global().async {
gate.wait() // stays blocked until the test releases it below
continuation.resume(returning: "late")
}
}
}
let elapsed = Date().timeIntervalSince(start)
XCTAssertNil(result, "must return nil at the timeout, not the blocked operation's value")
XCTAssertLessThan(elapsed, 5.0, "must not wait for the blocked operation to finish")
gate.signal() // release the background thread so nothing leaks
}
// MARK: - Source-invariant wiring guard
func testStateSnapshotUsesTheTimeoutFallback() throws {
let source = try bridgeSource()
guard let range = source.range(of: "private func liveAutomationSnapshot()") else {
throw XCTSkip("liveAutomationSnapshot not found")
}
// Bound to exactly this function (up to the next top-level func) rather than a
// fixed char count, so growth here can't silently push the patterns out of the
// window, and the assertions can't match strings from later functions.
let rest = source[range.upperBound...]
let bodyEnd = rest.range(of: "\nprivate func ")?.lowerBound ?? rest.endIndex
let body = String(rest[..<bodyEnd])
XCTAssertTrue(
body.contains("awaitWithTimeout(liveSnapshotMainActorTimeout"),
"/state must bound the MainActor hop with awaitWithTimeout")
XCTAssertTrue(
body.contains("cachedAutomationSnapshot()") && body.contains("snapshotStale = true"),
"/state must fall back to the cached snapshot (marked stale) on timeout")
}
private func bridgeSource() throws -> String {
let url = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Sources/DesktopAutomationBridge.swift")
return try String(contentsOf: url, encoding: .utf8)
}
}