|
| 1 | +import Foundation |
| 2 | +import Testing |
| 3 | + |
| 4 | +@Suite("macosdb subprocess smoke tests") |
| 5 | +struct SubprocessSmokeTests { |
| 6 | + |
| 7 | + @Test("--help exits zero and lists subcommands") |
| 8 | + func helpListsSubcommands() throws { |
| 9 | + let result = try runMacosdb(["--help"]) |
| 10 | + #expect(result.exitCode == 0) |
| 11 | + #expect(result.stdout.contains("scan")) |
| 12 | + #expect(result.stdout.contains("list")) |
| 13 | + #expect(result.stdout.contains("show")) |
| 14 | + #expect(result.stdout.contains("compare")) |
| 15 | + #expect(result.stdout.contains("validate")) |
| 16 | + } |
| 17 | + |
| 18 | + @Test("scan with missing archive exits non-zero") |
| 19 | + func scanMissingArchiveExitsNonZero() throws { |
| 20 | + let missing = NSTemporaryDirectory() + "definitely-not-here-\(UUID().uuidString).ipsw" |
| 21 | + let result = try runMacosdb(["scan", missing]) |
| 22 | + #expect(result.exitCode != 0) |
| 23 | + #expect(result.stderr.contains("Archive not found") || result.stdout.contains("Archive not found")) |
| 24 | + } |
| 25 | + |
| 26 | + @Test("scan --update-index without --release-date is rejected") |
| 27 | + func scanUpdateIndexRequiresReleaseDate() throws { |
| 28 | + let result = try runMacosdb(["scan", "archive.ipsw", "--update-index"]) |
| 29 | + #expect(result.exitCode != 0) |
| 30 | + #expect(result.stderr.contains("--release-date")) |
| 31 | + } |
| 32 | + |
| 33 | + @Test("validate with no arguments is rejected") |
| 34 | + func validateRequiresInput() throws { |
| 35 | + let result = try runMacosdb(["validate"]) |
| 36 | + #expect(result.exitCode != 0) |
| 37 | + #expect(result.stderr.contains("Provide at least one archive path or --dir")) |
| 38 | + } |
| 39 | + |
| 40 | + // MARK: - Helpers |
| 41 | + |
| 42 | + private struct ProcessResult { |
| 43 | + let exitCode: Int32 |
| 44 | + let stdout: String |
| 45 | + let stderr: String |
| 46 | + } |
| 47 | + |
| 48 | + private func runMacosdb(_ arguments: [String], file: String = #filePath) throws -> ProcessResult { |
| 49 | + let process = Process() |
| 50 | + process.executableURL = try Self.findBinary(testSourcePath: file) |
| 51 | + process.arguments = arguments |
| 52 | + |
| 53 | + let stdoutPipe = Pipe() |
| 54 | + let stderrPipe = Pipe() |
| 55 | + process.standardOutput = stdoutPipe |
| 56 | + process.standardError = stderrPipe |
| 57 | + |
| 58 | + try process.run() |
| 59 | + process.waitUntilExit() |
| 60 | + |
| 61 | + let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" |
| 62 | + let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" |
| 63 | + return ProcessResult(exitCode: process.terminationStatus, stdout: stdout, stderr: stderr) |
| 64 | + } |
| 65 | + |
| 66 | + /// Locate the built `macosdb` binary by walking up from the test source file |
| 67 | + /// to the package root, then searching the build output directories for the |
| 68 | + /// most recently built copy. Handles both `swift test` (binary lives under |
| 69 | + /// `.build/`) and `xcodebuild test` (under `DerivedData/Build/Products/`). |
| 70 | + /// Swift Testing's runner loads the .xctest dynamically, so `Bundle.allBundles` |
| 71 | + /// doesn't reliably contain it the way it does under XCTest. |
| 72 | + private static func findBinary(testSourcePath: String) throws -> URL { |
| 73 | + var dir = URL(fileURLWithPath: testSourcePath).deletingLastPathComponent() |
| 74 | + while dir.path != "/" { |
| 75 | + let packageSwift = dir.appendingPathComponent("Package.swift") |
| 76 | + if FileManager.default.fileExists(atPath: packageSwift.path) { |
| 77 | + return try locateBinary(under: dir) |
| 78 | + } |
| 79 | + dir.deleteLastPathComponent() |
| 80 | + } |
| 81 | + fatalError("Could not find Package.swift walking up from \(testSourcePath)") |
| 82 | + } |
| 83 | + |
| 84 | + private static func locateBinary(under packageRoot: URL) throws -> URL { |
| 85 | + let searchRoots = [".build", "DerivedData/Build/Products"] |
| 86 | + .map { packageRoot.appendingPathComponent($0) } |
| 87 | + .filter { FileManager.default.fileExists(atPath: $0.path) } |
| 88 | + |
| 89 | + let candidates = searchRoots.flatMap { root -> [URL] in |
| 90 | + let subpaths = (try? FileManager.default.subpathsOfDirectory(atPath: root.path)) ?? [] |
| 91 | + return subpaths |
| 92 | + .filter { $0.hasSuffix("/macosdb") || $0 == "macosdb" } |
| 93 | + .map { root.appendingPathComponent($0) } |
| 94 | + .filter { FileManager.default.isExecutableFile(atPath: $0.path) } |
| 95 | + } |
| 96 | + |
| 97 | + guard let binary = candidates.max(by: { lhs, rhs in |
| 98 | + let lhsDate = (try? lhs.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast |
| 99 | + let rhsDate = (try? rhs.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast |
| 100 | + return lhsDate < rhsDate |
| 101 | + }) else { |
| 102 | + fatalError("Could not find a built macosdb binary under \(packageRoot.path) — build the package first") |
| 103 | + } |
| 104 | + return binary |
| 105 | + } |
| 106 | +} |
0 commit comments