Files
Commilitia-Drop/ios/CDrop/Tests/LibWebRtcEngineTests.swift
T
admin 29e5c0a739 原生 macOS 客户端脚手架 + 元数据完整命名去缩写
- 命名去缩写:xcodegen 项目 + 四 target CDrop*→CommilitiaDrop*(CommilitiaDrop / …Share / …Widgets / …Tests);3 个 widget/control kind net.commilitia.cdrop.*→net.commilitia.Commilitia-Drop.*(旧装控件将成孤儿,需在设备重加);Tests @testable import CommilitiaDrop。刻意不动线协议 / 身份标识——bundle ID 家族、URL scheme cdrop://、Bonjour _cdrop._tcp、keychain service、DataChannel cdrop-file、entitlements 文件名——以免破跨端互通或丢会话
- 新增 macOS app target CommilitiaDropMac(platform macOS / deploymentTarget 26.0 / ARCHS arm64 / 与 iOS 同 bundle net.commilitia.Commilitia-Drop,同 App Store 记录 + 同 App Group):sources 仅 MacApp/,Info.plist 经 xcodegen 生成,本地 ad-hoc 免账号编译
- Mac 壳 MacApp/(Allman,#if os(macOS) 守卫,独立 @main 不与 iOS CDropApp 冲突):CommilitiaDropMacApp(@main + 单窗口 Window + windowResizability contentMinSize + 去 ⌘N + MenuBarExtra window 风格)、MacRootView(NavigationSplitView 侧栏五区骨架 传输/文件/消息/设备/设置 + 占位详情 MacPlaceholder + 品牌头)、MacMenuContent(菜单栏占位)
- Support/macOS/AppMac.entitlements:App Group group.net.commilitia.Commilitia-Drop 预置待用;macOS 下 App Group 属受限能力须 provisioning profile,故脚手架阶段 project.yml 暂不引用,待 Phase 6 Developer ID 签名再挂
- 根 Justfile:ios-sim-build / ios-device 跟随重命名(项目 / scheme / .app 路径);新增 mac-build(xcodegen + xcodebuild -sdk macosx,本地 ad-hoc)
- 验证:CommilitiaDropMac.app 与 CommilitiaDrop.app(iOS 模拟器)均 BUILD SUCCEEDED;WebRTC 走 -clonedSourcePackagesDirPath 复用已缓存 xcframework
2026-07-07 21:43:51 +08:00

141 lines
6.4 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
@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")
}
}