114 lines
3.3 KiB
Swift
114 lines
3.3 KiB
Swift
|
|
import XCTest
|
||
|
|
@testable import MacSyncShared
|
||
|
|
|
||
|
|
private struct FakeItem: Sendable, Equatable {
|
||
|
|
let id: String
|
||
|
|
let body: String
|
||
|
|
}
|
||
|
|
|
||
|
|
private actor FakeTransportState {
|
||
|
|
var pending: [FakeItem]
|
||
|
|
var reportedSent: [String] = []
|
||
|
|
var reportedFailed: [(id: String, error: String?)] = []
|
||
|
|
var fetchCalls: Int = 0
|
||
|
|
|
||
|
|
init(pending: [FakeItem]) {
|
||
|
|
self.pending = pending
|
||
|
|
}
|
||
|
|
|
||
|
|
func popAll() -> [FakeItem] {
|
||
|
|
fetchCalls += 1
|
||
|
|
let items = pending
|
||
|
|
pending = []
|
||
|
|
return items
|
||
|
|
}
|
||
|
|
|
||
|
|
func recordSent(_ id: String) { reportedSent.append(id) }
|
||
|
|
func recordFailed(_ id: String, _ error: String?) { reportedFailed.append((id, error)) }
|
||
|
|
}
|
||
|
|
|
||
|
|
private struct FakeTransport: SendQueueTransport {
|
||
|
|
typealias PendingItem = FakeItem
|
||
|
|
let state: FakeTransportState
|
||
|
|
|
||
|
|
func id(of item: FakeItem) -> String { item.id }
|
||
|
|
func fetchPending() async throws -> [FakeItem] { await state.popAll() }
|
||
|
|
func reportResult(id: String, status: String, error: String?) async throws {
|
||
|
|
if status == "sent" {
|
||
|
|
await state.recordSent(id)
|
||
|
|
} else {
|
||
|
|
await state.recordFailed(id, error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
final class SendQueueClientTests: XCTestCase {
|
||
|
|
@MainActor
|
||
|
|
func test_drainOnce_appliesAndAcksEachItem() async {
|
||
|
|
let state = FakeTransportState(pending: [
|
||
|
|
FakeItem(id: "a", body: "hello"),
|
||
|
|
FakeItem(id: "b", body: "world"),
|
||
|
|
])
|
||
|
|
let transport = FakeTransport(state: state)
|
||
|
|
|
||
|
|
let client = SendQueueClient(
|
||
|
|
label: "test",
|
||
|
|
transport: transport,
|
||
|
|
interval: 60
|
||
|
|
) { item in
|
||
|
|
item.body == "world" ? .failed(reason: "bad world") : .sent
|
||
|
|
}
|
||
|
|
|
||
|
|
await client.drainOnce()
|
||
|
|
|
||
|
|
let sent = await state.reportedSent
|
||
|
|
let failed = await state.reportedFailed
|
||
|
|
XCTAssertEqual(sent, ["a"])
|
||
|
|
XCTAssertEqual(failed.count, 1)
|
||
|
|
XCTAssertEqual(failed.first?.id, "b")
|
||
|
|
XCTAssertEqual(failed.first?.error, "bad world")
|
||
|
|
}
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
func test_drainOnce_isReentrantSafe() async {
|
||
|
|
let state = FakeTransportState(pending: (0..<5).map { FakeItem(id: "\($0)", body: "x") })
|
||
|
|
let transport = FakeTransport(state: state)
|
||
|
|
|
||
|
|
let client = SendQueueClient(
|
||
|
|
label: "test",
|
||
|
|
transport: transport,
|
||
|
|
interval: 60
|
||
|
|
) { _ in
|
||
|
|
try? await Task.sleep(nanoseconds: 20_000_000)
|
||
|
|
return .sent
|
||
|
|
}
|
||
|
|
|
||
|
|
async let a: Void = client.drainOnce()
|
||
|
|
async let b: Void = client.drainOnce()
|
||
|
|
async let c: Void = client.drainOnce()
|
||
|
|
_ = await (a, b, c)
|
||
|
|
|
||
|
|
let calls = await state.fetchCalls
|
||
|
|
XCTAssertEqual(calls, 1, "concurrent drains must coalesce")
|
||
|
|
let sent = await state.reportedSent
|
||
|
|
XCTAssertEqual(sent.count, 5)
|
||
|
|
}
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
func test_drainOnce_emptyQueueIsNoop() async {
|
||
|
|
let state = FakeTransportState(pending: [])
|
||
|
|
let transport = FakeTransport(state: state)
|
||
|
|
let client = SendQueueClient(
|
||
|
|
label: "test",
|
||
|
|
transport: transport,
|
||
|
|
interval: 60
|
||
|
|
) { _ in .sent }
|
||
|
|
|
||
|
|
await client.drainOnce()
|
||
|
|
let sent = await state.reportedSent
|
||
|
|
let failed = await state.reportedFailed
|
||
|
|
XCTAssertTrue(sent.isEmpty)
|
||
|
|
XCTAssertTrue(failed.isEmpty)
|
||
|
|
}
|
||
|
|
}
|