import Foundation import Testing @testable import RunnerCore /// Tests for ``XcodeInstall`` — the decidable parts of installing Xcode. /// /// The bug these exist for: the installer assumed the archive expanded to /// `Xcode.app`. A beta expands to `Xcode-beta.app`, so a 12 GB upload and a /// half-hour expansion both succeeded and then the final `mv` failed with /// "No such file or directory". Nothing about that is worth discovering from a /// forty-minute live run twice. @Suite("XcodeInstall") struct XcodeInstallTests { // MARK: - Discovering the expanded app @Test("a release archive's Xcode.app is found") func findsReleaseApp() throws { let path = try XcodeInstall.expandedAppPath( fromListing: "/tmp/xcode-expand/Xcode.app\n", staging: "/tmp/xcode-expand") #expect(path == "/tmp/xcode-expand/Xcode.app") } /// The reported failure, in one line. @Test("a beta archive's Xcode-beta.app is found") func findsBetaApp() throws { let path = try XcodeInstall.expandedAppPath( fromListing: "/tmp/xcode-expand/Xcode-beta.app", staging: "/tmp/xcode-expand") #expect(path == "/tmp/xcode-expand/Xcode-beta.app") } @Test("a version-qualified name is found") func findsVersionQualifiedApp() throws { let path = try XcodeInstall.expandedAppPath( fromListing: " /tmp/xcode-expand/Xcode_16.2.app \n\n", staging: "/tmp/xcode-expand") #expect(path == "/tmp/xcode-expand/Xcode_16.2.app") } /// `/bin/sh` echoes an unmatched glob back verbatim, so "no match" arrives /// as a plausible-looking path rather than as empty output. @Test("an unmatched glob reads as no match, not as a path") func unmatchedGlobIsNotAPath() { #expect(throws: CoreError.self) { try XcodeInstall.expandedAppPath( fromListing: "/tmp/xcode-expand/*.app\n", staging: "/tmp/xcode-expand") } } @Test("empty output is an error naming the staging directory") func emptyListingThrows() { do { _ = try XcodeInstall.expandedAppPath(fromListing: "\n \n", staging: "/tmp/xcode-expand") Issue.record("expected a failure") } catch { #expect("\(error)".contains("/tmp/xcode-expand")) } } /// Guessing between two candidates would install something the operator did /// not ask for, so this stops instead. @Test("several apps is an error listing them") func ambiguousListingThrows() { do { _ = try XcodeInstall.expandedAppPath( fromListing: "/tmp/x/Xcode.app\n/tmp/x/Xcode-beta.app\n", staging: "/tmp/x") Issue.record("expected a failure") } catch { let text = "\(error)" #expect(text.contains("Xcode.app")) #expect(text.contains("Xcode-beta.app")) } } // MARK: - Disk arithmetic @Test("the requirement covers the archive and its expansion") func requiredFreeSpaceCoversBoth() { let xip = 12_000_000_000 #expect(XcodeInstall.expansionEstimateBytes(xipBytes: xip) == 42_000_000_000) #expect(XcodeInstall.requiredFreeBytes(xipBytes: xip) == 54_000_000_000) } @Test("df -Pk output yields available bytes") func parsesDF() { let output = """ Filesystem 1024-blocks Used Available Capacity Mounted on /dev/disk3s5 488245288 120000000 62914560 66% / """ #expect(XcodeInstall.availableBytes(dfOutput: output) == 62_914_560 * 1024) } /// Unparseable output means "could not check", not "no space" — the caller /// must not refuse to install because `df` printed something unexpected. @Test("unparseable df output yields nil rather than zero") func unparseableDFIsNil() { #expect(XcodeInstall.availableBytes(dfOutput: "df: /nope: No such file or directory") == nil) #expect(XcodeInstall.availableBytes(dfOutput: "") == nil) } @Test("the shortfall message names every number and a remedy") func messageNamesTheNumbers() { let text = XcodeInstall.insufficientDiskMessage( xipBytes: 12_000_000_000, availableBytes: 20_000_000_000) #expect(text.contains("20.0 GB")) // available #expect(text.contains("54.0 GB")) // needed #expect(text.contains("12.0 GB")) // the archive #expect(text.contains("--disk-gb")) } @Test("byte counts render as decimal gigabytes") func formatsGigabytes() { #expect(XcodeInstall.formatGB(12_400_000_000) == "12.4 GB") #expect(XcodeInstall.formatGB(0) == "0.0 GB") } }