import XCTest @testable import CommilitiaDrop // LibWebRtcEngine 环回端到端测试:两个引擎在同进程内交叉连,跑完整线协议 // (offer/answer/ICE + meta/分片/done + ack + 收尾),验证收到的文件字节与源逐字节相等。 // 这是真机部署前最强的非设备验证——证实线协议端口(分片 / 背压 / ack / finalize)在 libwebrtc 上 // 正确闭环;不覆盖真实网络 / 吞吐(须真机)。host 候选环回直连,无需 STUN/TURN / 本地网络权限。 final class LibWebRtcEngineTests: XCTestCase { // 占位 delegate:仅供 LibWebRtcEngine 构造(随后重定向到 Peer)。 final class NullDelegate: P2PEngineDelegate { static let shared = NullDelegate() func engineDidProgress(sessionId: String, bytes: Int64) {} func engineDidChangeState(sessionId: String, state: String) {} func engineDidEmitSignal(sessionId: String, toPeer: String, payloadJSON: String) {} func engineDidSave(sessionId: String, path: String) {} func engineDidSelectIcePair(sessionId: String, local: String, remote: String) {} func engineDidLog(_ line: String) {} } // 环回事件宿主:捕获状态 / 落盘,并把出站信令「串行」转发给对端引擎——每条等对端 handleSignal // 完成再投下一条,复刻 p2pIos.ts 的信令链语义(offer/answer 必先于其后 trickle 的 ICE 候选, // 否则 addIceCandidate 在 remote description 未设时报错)。 final class Peer: P2PEngineDelegate { let name: String // 对端索引「我」用的设备名(转发 handleSignal 时填 fromPeer) let engine: LibWebRtcEngine weak var other: Peer? private let signalQueue: DispatchQueue var onState: ((String) -> Void)? var onSaved: ((String) -> Void)? init(name: String, downloadDir: String) { self.name = name signalQueue = DispatchQueue(label: "test.signal.\(name)") engine = LibWebRtcEngine(downloadDir: downloadDir, delegate: NullDelegate.shared) engine.delegate = self // 重定向到自己(self 此刻全属性已初始化,可用) } func engineDidProgress(sessionId: String, bytes: Int64) {} func engineDidChangeState(sessionId: String, state: String) { onState?(state) } // 出站信令 → 对端引擎,按对端串行队列逐条投递(等对端处理完再投下一条)。 func engineDidEmitSignal(sessionId: String, toPeer: String, payloadJSON: String) { guard let other else { return } let fromPeer = name other.signalQueue.async { [weak other] in guard let other else { return } let sem = DispatchSemaphore(value: 0) other.engine.handleSignal(fromPeer: fromPeer, payloadJSON: payloadJSON) { _ in sem.signal() } sem.wait() } } func engineDidSave(sessionId: String, path: String) { onSaved?(path) } func engineDidSelectIcePair(sessionId: String, local: String, remote: String) {} func engineDidLog(_ line: String) {} } // 端到端:20 MB 伪随机源(跨数百个 64KB 分片 + 触发 HIGH/LOW 背压),环回传输后逐字节比对。 func testLoopbackTransferIntegrity() throws { let fm = FileManager.default let dirA = fm.temporaryDirectory.appendingPathComponent("cdropTestA-\(UUID().uuidString)") let dirB = fm.temporaryDirectory.appendingPathComponent("cdropTestB-\(UUID().uuidString)") try fm.createDirectory(at: dirA, withIntermediateDirectories: true) try fm.createDirectory(at: dirB, withIntermediateDirectories: true) defer { try? fm.removeItem(at: dirA); try? fm.removeItem(at: dirB) } // 确定可重现的伪随机字节(线性同余),便于失败诊断。 let size = 20 * 1024 * 1024 var payload = Data(count: size) payload.withUnsafeMutableBytes { raw in let p = raw.bindMemory(to: UInt8.self) var s: UInt64 = 0x9E37_79B9_7F4A_7C15 for i in 0 ..< size { s = s &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407 p[i] = UInt8(truncatingIfNeeded: s >> 33) } } let srcURL = dirA.appendingPathComponent("payload.bin") try payload.write(to: srcURL) let senderName = "DeviceA" let receiverName = "DeviceB" let peerA = Peer(name: senderName, downloadDir: dirA.path) // 发送端(engineB 据 senderName 索引) let peerB = Peer(name: receiverName, downloadDir: dirB.path) // 接收端(engineA 据 receiverName 索引) peerA.other = peerB peerB.other = peerA let senderDone = expectation(description: "sender completed") let receiverSaved = expectation(description: "receiver saved") var savedPath: String? peerA.onState = { state in if state == "completed" { senderDone.fulfill() } else if state == "failed" { XCTFail("sender entered failed state") } } peerB.onSaved = { path in savedPath = path receiverSaved.fulfill() } peerB.onState = { state in if state == "failed" { XCTFail("receiver entered failed state") } } let sid = "loopback-session-1" let ice = "[]" // host 候选环回直连,无 STUN/TURN // 接收端先建 PeerConnection 等 offer(生产中 transfer:incoming 先于 offer 到达)。 peerB.engine.startIncoming(sessionId: sid, peerName: senderName, iceServersJSON: ice) { err in XCTAssertNil(err, "startIncoming error: \(String(describing: err))") } peerA.engine.startOutgoing(sessionId: sid, peerName: receiverName, filePath: srcURL.path, iceServersJSON: ice) { err in XCTAssertNil(err, "startOutgoing error: \(String(describing: err))") } wait(for: [senderDone, receiverSaved], timeout: 120) let saved = try XCTUnwrap(savedPath, "receiver never reported a saved path") let got = try Data(contentsOf: URL(fileURLWithPath: saved)) XCTAssertEqual(got.count, size, "received size mismatch") XCTAssertEqual(got, payload, "received bytes differ from source") } }