Files
share/apps/ios/Share/SelfTest.swift
T
TZGyn decaaa2de1 Use the tzgyn relay as the default backend
The CLI and iOS app now point at share.tzgyn.dev:9009 instead of the upstream croc relay. Both were verified against the deployed relay; local relays remain available through --relay and the app's Relay setting.
2026-10-26 22:32:15 +08:00

182 lines
6.2 KiB
Swift

import Foundation
import Share
import SwiftUI
/// A headless-ish smoke test driven by launch arguments:
///
/// xcrun simctl launch <udid> dev.tzgyn.share \
/// -shareSelfTest -shareSelfTestRelay 127.0.0.1:9009 \
/// -shareSelfTestCode 1234-selftest-share-apple
///
/// The test starts a receive room, waits for one complete file, and writes
/// `Documents/share-selftest.txt`. Console output is unreliable from a
/// simulator and impossible to read from a device, so the report on disk is
/// the source of truth.
@MainActor
enum SelfTest {
private static var started = false
static func runIfRequested(model: AppModel) {
let args = ProcessInfo.processInfo.arguments
guard !started else { return }
let relay = value(after: "-shareSelfTestRelay", in: args) ?? ShareRelayDefault()
let code = value(after: "-shareSelfTestCode", in: args)
?? (args.contains("-shareSelfTestSend")
? "1234-selftest-send-apple"
: "1234-selftest-share-apple")
if args.contains("-shareSelfTestSend") {
started = true
runSend(model: model, relay: relay, code: code)
} else if args.contains("-shareSelfTest") {
started = true
runReceive(model: model, relay: relay, code: code)
}
}
private static func runReceive(model: AppModel, relay: String, code: String) {
model.mode = .receive
model.relay = relay
model.code = code
model.startReceive()
Task { @MainActor in
let deadline = Date().addingTimeInterval(120)
var lastStatus: RoomStatus?
while Date() < deadline {
try? await Task.sleep(nanoseconds: 500_000_000)
if let status = model.status {
lastStatus = status
if status.allComplete {
writeReport(result: "ok", model: model, status: status, error: nil)
return
}
if !status.running, let error = status.error, !error.isEmpty {
writeReport(result: "room-error", model: model, status: status, error: error)
return
}
}
if model.status == nil, let error = model.errorMessage, !error.isEmpty {
writeReport(result: "start-error", model: model, status: nil, error: error)
return
}
}
writeReport(
result: "timeout",
model: model,
status: lastStatus,
error: "no file completed before the 120s deadline"
)
}
}
private static func runSend(model: AppModel, relay: String, code: String) {
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let seedDir = documents.appendingPathComponent("Seeds/selftest-send", isDirectory: true)
let seedURL = seedDir.appendingPathComponent("share-send-selftest.bin")
do {
try FileManager.default.createDirectory(at: seedDir, withIntermediateDirectories: true)
let data = Data(repeating: 0x5A, count: 2 * 1024 * 1024)
try data.write(to: seedURL)
} catch {
writeReport(result: "seed-error", model: model, status: nil, error: error.localizedDescription)
return
}
model.mode = .send
model.relay = relay
model.code = code
model.pickedFiles = [
PickedFile(url: seedURL, name: seedURL.lastPathComponent, size: 2 * 1024 * 1024)
]
model.startSend()
Task { @MainActor in
let deadline = Date().addingTimeInterval(120)
var lastStatus: RoomStatus?
var sawPeer = false
while Date() < deadline {
try? await Task.sleep(nanoseconds: 500_000_000)
if let status = model.status {
lastStatus = status
if status.peers > 0 { sawPeer = true }
if sawPeer, status.allComplete {
// Give the receiver a moment to finish the last chunks and
// disconnect before recording the result.
try? await Task.sleep(nanoseconds: 1_000_000_000)
writeReport(result: "ok-seeded", model: model, status: status, error: nil)
return
}
if !status.running, let error = status.error, !error.isEmpty {
writeReport(result: "room-error", model: model, status: status, error: error)
return
}
}
if model.status == nil, let error = model.errorMessage, !error.isEmpty {
writeReport(result: "start-error", model: model, status: nil, error: error)
return
}
}
writeReport(
result: "timeout",
model: model,
status: lastStatus,
error: "no peer completed the seed before the 120s deadline"
)
}
}
private static func value(after flag: String, in args: [String]) -> String? {
guard let index = args.firstIndex(of: flag), index + 1 < args.count else { return nil }
return args[index + 1]
}
private static func writeReport(
result: String,
model: AppModel,
status: RoomStatus?,
error: String?
) {
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let url = documents.appendingPathComponent("share-selftest.txt")
var lines = [
"result=\(result)",
"code=\(status?.code ?? model.code)",
"relay=\(status?.relay ?? model.relay)",
"running=\(status?.running ?? false)",
"peers=\(status?.peers ?? 0)",
"files=\(status?.files.count ?? 0)",
]
if let error, !error.isEmpty {
lines.append("error=\(error.replacingOccurrences(of: "\n", with: " "))")
}
for file in status?.files ?? [] {
let localURL: URL?
if model.mode == .send {
localURL = model.pickedFiles.first(where: { $0.name == file.name })?.url
} else {
localURL = model.receivedURL(for: file)
}
let exists = localURL.map { FileManager.default.fileExists(atPath: $0.path) } ?? false
let size = localURL.flatMap {
try? $0.resourceValues(forKeys: [.fileSizeKey]).fileSize
} ?? 0
lines.append(
"file=\(file.name) size=\(file.size) chunks=\(file.haveChunks)/\(file.totalChunks) complete=\(file.complete) exists=\(exists) diskSize=\(size)"
)
}
try? lines.joined(separator: "\n").appending("\n").write(
to: url,
atomically: true,
encoding: .utf8
)
print("Share self-test \(result): \(url.path)")
}
}