mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 18:39:55 +02:00
The Cancel button on an in-progress receive did nothing. CoreRepository funnelled every core call through one serial DispatchQueue, but `receive` is a blocking core call that occupies that queue for the whole transfer. The tapped `cancelTransfer` was enqueued behind the in-flight `receive` on the same serial queue, so it could never run until `receive` returned — which it never would, because it was waiting to be cancelled. A deadlock the button couldn't escape. The Rust core is explicitly designed for cancel to arrive from another thread mid-receive (VnidropCore.block_on uses a shared runtime handle for exactly this). Extracts the two-lane dispatch into a CoreDispatcher: a serial lane for ordered calls and a separate concurrent lane for interrupt-style calls, and routes cancel through the latter so the signal reaches the core and unblocks the receive. Adds CoreDispatcherTests, including a regression guard that an interrupt completes while the serial lane is blocked.
51 lines
1.6 KiB
Swift
51 lines
1.6 KiB
Swift
import XCTest
|
|
@testable import VniDrop
|
|
|
|
final class CoreDispatcherTests: XCTestCase {
|
|
|
|
/// Regression guard for the receive-cancel deadlock: an interrupt-lane call
|
|
/// must complete even while the serial lane is occupied by a blocking call.
|
|
/// With a single shared queue (the old design) the interrupt would be stuck
|
|
/// behind the blocked `receive`, and this would time out.
|
|
func testInterruptCompletesWhileSerialLaneIsBlocked() async {
|
|
let dispatcher = CoreDispatcher()
|
|
let serialEntered = DispatchSemaphore(value: 0)
|
|
let releaseSerial = DispatchSemaphore(value: 0)
|
|
|
|
// Occupy the serial lane with a call that blocks until we release it.
|
|
let serialTask = Task {
|
|
await dispatcher.run {
|
|
serialEntered.signal()
|
|
releaseSerial.wait()
|
|
}
|
|
}
|
|
XCTAssertEqual(serialEntered.wait(timeout: .now() + 2), .success, "serial lane never started")
|
|
|
|
// The interrupt lane must run despite the serial lane being blocked.
|
|
let interruptDone = DispatchSemaphore(value: 0)
|
|
Task.detached {
|
|
_ = await dispatcher.runInterrupt { 42 }
|
|
interruptDone.signal()
|
|
}
|
|
XCTAssertEqual(
|
|
interruptDone.wait(timeout: .now() + 2), .success,
|
|
"interrupt lane was blocked behind the occupied serial lane")
|
|
|
|
releaseSerial.signal()
|
|
_ = await serialTask.value
|
|
}
|
|
|
|
func testRunPropagatesValuesAndErrors() async {
|
|
let dispatcher = CoreDispatcher()
|
|
|
|
let value = await dispatcher.run { 7 }
|
|
XCTAssertEqual(try? value.get(), 7)
|
|
|
|
let failure = await dispatcher.run { () -> Int in throw TestError.unimplemented }
|
|
switch failure {
|
|
case .success: XCTFail("expected the thrown error to propagate")
|
|
case .failure(let error): XCTAssertTrue(error is TestError)
|
|
}
|
|
}
|
|
}
|