Files
Commilitia-Drop/ios/CDrop/Tests/LibWebRtcEngineTests.swift
T
admin 9d46e26f50 原生数据面 macOS 冒烟通过(P0 闸)+ 修 stasel WebRTC macOS 切片缺头
- P0 闸达成:新增 macOS 独立单测 target CommilitiaDropMacTests(把 LibWebRtcEngine.swift 直接编入测试 bundle,无宿主 app、同模块无需 @testable),跑与 iOS 同一份环回端到端测试——两个引擎同进程交叉连,20MB 伪随机源逐字节比对。macOS 上线协议(offer/answer/ICE + 分片 / 背压 / ack / finalize)在 libwebrtc 闭环,证实原生数据面在 Mac 可用(≈3s 通过)。是 Mac 复用 iOS 引擎的技术前提
- 修 stasel/WebRTC 149.0.0 原生 macOS 切片缺头:macos-x86_64_arm64 随包只带 umbrella WebRTC.h、缺全部 80+ 子头(ios / maccatalyst / simulator 三切片各 93 头齐全),umbrella #import 的子头找不到致 import WebRTC 在原生 macOS 编不过;切片二进制本身有效(arm64+x86_64 dylib)。scripts/patch-macos-webrtc-headers.sh 从同包 ios-arm64 切片补齐(子头为平台守卫 ObjC 声明、跨切片同构;排除 3 个 UIKit 绑定视图头 + 从 umbrella 去掉本包缺失的 RTCMTLNSVideoView.h)。幂等,须 SPM resolve 之后、build 之前跑
- LibWebRtcEngineTests.swift 导入条件化:仅 #if os(iOS) 时 @testable import CommilitiaDrop(macOS 侧引擎编在同模块、无需 import),iOS 与 macOS 共用同一份测试文件验证同构闭环
- Justfile:mac-prep(xcodegen + -resolvePackageDependencies + 补头脚本)→ mac-build / mac-test 均依赖之;build / test 加 -disableAutomaticPackageResolution 防 build 期再解析冲掉补丁
- 验证:just mac-test 从残缺态复现全绿——resolve + 补头(1→90)+ 编译 + 环回测试通过
2026-07-07 21:59:35 +08:00

146 lines
6.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
// iOS宿 app @testable import LibWebRtcEngine app
// macOSCommilitiaDropMacTests LibWebRtcEngine.swift bundle import
// 线 libwebrtc iOS macOS iOS
#if os(iOS)
@testable import CommilitiaDrop
#endif
// 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")
}
}