import Foundation import Share import SwiftUI /// A headless-ish smoke test driven by launch arguments: /// /// xcrun simctl launch 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)") } }