mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 17:44:50 +00:00
85 lines
2.4 KiB
Swift
85 lines
2.4 KiB
Swift
import XCTest
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
/// Pause one retry/debounce phase; subsequent passes remain asynchronous without wall-clock delay.
|
|
@MainActor
|
|
final class RuleRuntimeDelayGate {
|
|
private let started: XCTestExpectation
|
|
private let onTimeout: () -> Void
|
|
private var didPause = false
|
|
private let stream: AsyncStream<Void>
|
|
private let continuation: AsyncStream<Void>.Continuation
|
|
|
|
init(
|
|
started: XCTestExpectation,
|
|
onTimeout: @escaping () -> Void = {
|
|
XCTFail("Timed out waiting for rule runtime delay release")
|
|
}
|
|
) {
|
|
self.started = started
|
|
self.onTimeout = onTimeout
|
|
(stream, continuation) = AsyncStream<Void>.makeStream()
|
|
}
|
|
|
|
func wait(timeout: Duration = .seconds(10)) async throws {
|
|
try Task.checkCancellation()
|
|
guard !didPause else {
|
|
await Task.yield()
|
|
return
|
|
}
|
|
didPause = true
|
|
started.fulfill()
|
|
// AsyncStream ends its iterator on cancellation, so the timer race can drain both children.
|
|
do {
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
group.addTask { [stream] in
|
|
for await _ in stream {}
|
|
try Task.checkCancellation()
|
|
}
|
|
group.addTask {
|
|
try await Task.sleep(for: timeout)
|
|
throw TestWaitError.timedOut("rule runtime delay release")
|
|
}
|
|
defer { group.cancelAll() }
|
|
try await group.next()
|
|
}
|
|
} catch let error as TestWaitError {
|
|
onTimeout()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func resume() {
|
|
continuation.finish()
|
|
}
|
|
}
|
|
|
|
/// Rule application can replace its task handle while an earlier save is finishing.
|
|
@MainActor
|
|
func withRuleApplicationTask<Value: Sendable>(
|
|
_ task: Task<Value, Never>, controller: ProxyRuntimeController, release: () -> Void,
|
|
body: () async throws -> Void
|
|
) async throws {
|
|
try await withTestTask(
|
|
task, release: release,
|
|
finish: {
|
|
controller.ruleApplicationTask?.cancel()
|
|
await controller.ruleApplicationTask?.value
|
|
}, body: body)
|
|
}
|
|
|
|
final class PausedRuntimeRuleSource: @unchecked Sendable {
|
|
let started: XCTestExpectation
|
|
private let release = TestAsyncGate("rule source release")
|
|
init(started: XCTestExpectation) { self.started = started }
|
|
func resume() { release.release() }
|
|
func load(_ source: RulesSource) async -> RuleSnapshot {
|
|
if source == .geolocationCN {
|
|
started.fulfill()
|
|
_ = await release.wait()
|
|
}
|
|
return rulesFixture(source)
|
|
}
|
|
}
|