From a30a8fc551d69fc6402f40a84bf3f89572801427 Mon Sep 17 00:00:00 2001 From: Gronod Date: Fri, 11 Sep 2026 21:29:16 +0100 Subject: [PATCH] test(m9): complete migration of all remaining test suites to XCTest Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../ArgyllRunnerCalibrationTests.swift | 36 +- .../ArgyllRunnerColprofTests.swift | 23 +- .../ArgyllRunnerStreamingLoopTests.swift | 67 ++- .../CalibrationStoreTests.swift | 28 +- .../ICCeryCoreTests/JSONFileStoreTests.swift | 35 +- Tests/ICCeryCoreTests/PresetTests.swift | 465 +++++++++--------- .../PresetViewModelMappingTests.swift | 70 ++- Tests/ICCeryCoreTests/PrintPanelTests.swift | 30 +- Tests/ICCeryCoreTests/PrinttargTests.swift | 41 +- .../ICCeryCoreTests/ProcessManagerTests.swift | 189 +++---- .../ProcessRunSupportTests.swift | 27 +- .../ProfileInstallerTests.swift | 65 ++- Tests/ICCeryCoreTests/SettingsTests.swift | 111 ++--- Tests/ICCeryCoreTests/TargenTests.swift | 31 +- .../TargetWorkflowViewModelTests.swift | 23 +- .../VerificationHistoryStoreTests.swift | 62 +-- .../WizardCalibrationSessionTests.swift | 38 +- Tests/ICCeryCoreTests/WizardGatingTests.swift | 96 ++-- 18 files changed, 685 insertions(+), 752 deletions(-) diff --git a/Tests/ICCeryCoreTests/ArgyllRunnerCalibrationTests.swift b/Tests/ICCeryCoreTests/ArgyllRunnerCalibrationTests.swift index 204fdc0..cf89008 100644 --- a/Tests/ICCeryCoreTests/ArgyllRunnerCalibrationTests.swift +++ b/Tests/ICCeryCoreTests/ArgyllRunnerCalibrationTests.swift @@ -1,9 +1,8 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("ArgyllRunner Calibration") -struct ArgyllRunnerCalibrationTests { +final class ArgyllRunnerCalibrationTests: XCTestCase { private func makeRunner(processManager: ProcessManager = ProcessManager()) -> ArgyllRunner { let binDir = URL(fileURLWithPath: #filePath) @@ -23,8 +22,7 @@ struct ArgyllRunnerCalibrationTests { return root } - @Test("Calibration targen produces CAL_*.ti1") - func calibrationTargenProducesTi1() async throws { + func testCalibrationTargenProducesTi1() async throws { let testRoot = try makeTestDir() let runner = makeRunner() let config = CalibrationTargenConfig( @@ -36,13 +34,12 @@ struct ArgyllRunnerCalibrationTests { let url = try await runner.runCalibrationTargen(config: config) - #expect(url.lastPathComponent == "CAL_demo.ti1") - #expect(FileManager.default.fileExists(atPath: url.path)) + XCTAssertEqual(url.lastPathComponent, "CAL_demo.ti1") + XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) try? FileManager.default.removeItem(at: testRoot) } - @Test("Calibration targen from foo runs as process id targen_CAL_foo") - func calibrationTargenProcessId() async throws { + func testCalibrationTargenProcessId() async throws { let testRoot = try makeTestDir() let pm = ProcessManager() let runner = makeRunner(processManager: pm) @@ -65,13 +62,13 @@ struct ArgyllRunnerCalibrationTests { let url = try await runner.runCalibrationTargen(config: config) - #expect(url.lastPathComponent == "CAL_foo.ti1") - #expect(await sawExit.value) + XCTAssertEqual(url.lastPathComponent, "CAL_foo.ti1") + let sawExitEvent = await sawExit.value + XCTAssertTrue(sawExitEvent) try? FileManager.default.removeItem(at: testRoot) } - @Test("printcal captured run creates .cal") - func printcalProducesCal() async throws { + func testPrintcalProducesCal() async throws { let testRoot = try makeTestDir() let runner = makeRunner() let output = testRoot.appendingPathComponent("CAL_demo.cal") @@ -83,13 +80,12 @@ struct ArgyllRunnerCalibrationTests { let url = try await runner.runPrintcal(config: config) - #expect(url.lastPathComponent == "CAL_demo.cal") - #expect(FileManager.default.fileExists(atPath: url.path)) + XCTAssertEqual(url.lastPathComponent, "CAL_demo.cal") + XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) try? FileManager.default.removeItem(at: testRoot) } - @Test("printcal failure throws toolFailed") - func printcalFailureThrows() async throws { + func testPrintcalFailureThrows() async throws { let testRoot = try makeTestDir() defer { try? FileManager.default.removeItem(at: testRoot) } @@ -117,9 +113,11 @@ struct ArgyllRunnerCalibrationTests { outputURL: output ) - await #expect(throws: ArgyllRunnerError.toolFailed( - tool: "printcal", code: 1, logs: ["printcal mock failure\n"])) { + await assertAsyncThrows(expectedType: ArgyllRunnerError.self) { _ = try await runner.runPrintcal(config: config) + } errorHandler: { error in + XCTAssertEqual(error, .toolFailed( + tool: "printcal", code: 1, logs: ["printcal mock failure\n"])) } } } diff --git a/Tests/ICCeryCoreTests/ArgyllRunnerColprofTests.swift b/Tests/ICCeryCoreTests/ArgyllRunnerColprofTests.swift index 6cf0d4e..1769fc4 100644 --- a/Tests/ICCeryCoreTests/ArgyllRunnerColprofTests.swift +++ b/Tests/ICCeryCoreTests/ArgyllRunnerColprofTests.swift @@ -1,5 +1,5 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore final class LogHolder: @unchecked Sendable { @@ -19,11 +19,9 @@ final class LogHolder: @unchecked Sendable { } } -@Suite("ArgyllRunner colprof") -struct ArgyllRunnerColprofTests { +final class ArgyllRunnerColprofTests: XCTestCase { - @Test("Mock colprof produces .icc") - func colprofProducesIcc() async throws { + func testColprofProducesIcc() async throws { let binDir = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() .deletingLastPathComponent() @@ -43,15 +41,14 @@ struct ArgyllRunnerColprofTests { holder.append(batch) } - #expect(url.lastPathComponent == "testrun.icc") - #expect(FileManager.default.fileExists(atPath: url.path)) - #expect(holder.lines.contains { $0.contains("Gamut mapping") }) + XCTAssertEqual(url.lastPathComponent, "testrun.icc") + XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) + XCTAssertTrue(holder.lines.contains { $0.contains("Gamut mapping") }) try? FileManager.default.removeItem(at: testRoot) } - @Test("Failing colprof throws toolFailed with code and logs") - func colprofFailureThrowsToolFailed() async throws { + func testColprofFailureThrowsToolFailed() async throws { let dir = FileManager.default.temporaryDirectory .appendingPathComponent("colprof-fail-\(UUID().uuidString)") try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) @@ -72,9 +69,11 @@ struct ArgyllRunnerColprofTests { ) let config = ColprofConfig(basename: "failrun", workingDirectory: dir) - await #expect(throws: ArgyllRunnerError.toolFailed( - tool: "colprof", code: 4, logs: ["colprof broke"])) { + await assertAsyncThrows(expectedType: ArgyllRunnerError.self) { try await runner.runColprof(config: config) + } errorHandler: { error in + XCTAssertEqual(error, .toolFailed( + tool: "colprof", code: 4, logs: ["colprof broke"])) } } } diff --git a/Tests/ICCeryCoreTests/ArgyllRunnerStreamingLoopTests.swift b/Tests/ICCeryCoreTests/ArgyllRunnerStreamingLoopTests.swift index c1d114b..083401a 100644 --- a/Tests/ICCeryCoreTests/ArgyllRunnerStreamingLoopTests.swift +++ b/Tests/ICCeryCoreTests/ArgyllRunnerStreamingLoopTests.swift @@ -1,5 +1,5 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore /// Focused contracts for the shared `runStreamingTool` loop (#79). @@ -7,8 +7,7 @@ import Testing /// Every test uses a per-test temporary directory, unique basenames, /// and a fresh `ProcessManager` — no shared UI fixture scripts and no /// process-environment mutation. -@Suite("ArgyllRunner streaming loop contracts") -struct ArgyllRunnerStreamingLoopTests { +final class ArgyllRunnerStreamingLoopTests: XCTestCase { private func makeTempDir() throws -> URL { let dir = FileManager.default.temporaryDirectory @@ -30,8 +29,7 @@ struct ArgyllRunnerStreamingLoopTests { binaryResolver: BinaryResolver(bundledRoot: binDir, overrideDir: binDir)) } - @Test("Non-zero exit throws toolFailed retaining code and collected stdout/stderr lines") - func nonZeroExitThrowsToolFailed() async throws { + func testNonZeroExitThrowsToolFailed() async throws { let dir = try makeTempDir() defer { try? FileManager.default.removeItem(at: dir) } try writeMock("targen", """ @@ -47,21 +45,20 @@ struct ArgyllRunnerStreamingLoopTests { do { _ = try await runner.runTargen(config: config) - Issue.record("Expected toolFailed") + XCTFail("Expected toolFailed") } catch let error as ArgyllRunnerError { guard case .toolFailed(let tool, let code, let logs) = error else { - Issue.record("Expected toolFailed, got \(error)") + XCTFail("Expected toolFailed, got \(error)") return } - #expect(tool == "targen") - #expect(code == 3) - #expect(logs.contains("Generating patches...")) - #expect(logs.contains("targen: too few patches")) + XCTAssertEqual(tool, "targen") + XCTAssertEqual(code, 3) + XCTAssertTrue(logs.contains("Generating patches...")) + XCTAssertTrue(logs.contains("targen: too few patches")) } } - @Test("Exit 0 without expected artefact throws missingArtefact with the artefact path") - func zeroExitMissingArtefact() async throws { + func testZeroExitMissingArtefact() async throws { let dir = try makeTempDir() defer { try? FileManager.default.removeItem(at: dir) } try writeMock("targen", """ @@ -75,13 +72,14 @@ struct ArgyllRunnerStreamingLoopTests { colourSpace: .rgb, patchCount: 800, whitePatches: 4, blackPatches: 4, basename: "gone", workingDirectory: dir) - await #expect(throws: ArgyllRunnerError.missingArtefact(expectedPath)) { + await assertAsyncThrows(expectedType: ArgyllRunnerError.self) { try await runner.runTargen(config: config) + } errorHandler: { error in + XCTAssertEqual(error, .missingArtefact(expectedPath)) } } - @Test("Immediate exit after one stdout line still delivers the line and succeeds") - func immediateExitDeliversLine() async throws { + func testImmediateExitDeliversLine() async throws { let dir = try makeTempDir() defer { try? FileManager.default.removeItem(at: dir) } try writeMock("targen", """ @@ -101,13 +99,12 @@ struct ArgyllRunnerStreamingLoopTests { let url = try await runner.runTargen(config: config) { batch in holder.append(batch) } - #expect(url.lastPathComponent == "quick.ti1") - #expect(FileManager.default.fileExists(atPath: url.path)) - #expect(holder.lines.contains("only line")) + XCTAssertEqual(url.lastPathComponent, "quick.ti1") + XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) + XCTAssertTrue(holder.lines.contains("only line")) } - @Test("colprof unterminated progress fragment reaches onLogBatch before exit") - func colprofPartialLineFlush() async throws { + func testColprofPartialLineFlush() async throws { let dir = try makeTempDir() defer { try? FileManager.default.removeItem(at: dir) } // The fragment is printed without a newline, then the mock sleeps @@ -129,13 +126,13 @@ struct ArgyllRunnerStreamingLoopTests { let url = try await runner.runColprof(config: config) { batch in holder.append(batch) } - #expect(url.lastPathComponent == "frag.icc") - #expect(FileManager.default.fileExists(atPath: url.path)) - #expect(holder.lines.contains("Doing gamut mapping")) + XCTAssertEqual(url.lastPathComponent, "frag.icc") + XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) + XCTAssertTrue(holder.lines.contains("Doing gamut mapping")) } - @Test("toolFailed maps each tool to its user-facing description", - arguments: [ + func testToolDescriptions() { + let cases: [(tool: String, expected: String)] = [ (tool: "chartread", expected: "Chartread failed: boom"), (tool: "average", expected: "Averaging failed: boom"), (tool: "colprof", expected: "Profile creation failed: boom"), @@ -143,19 +140,19 @@ struct ArgyllRunnerStreamingLoopTests { (tool: "applycal", expected: "Apply calibration failed: boom"), (tool: "iccgamut", expected: "Gamut extraction failed: boom"), (tool: "profcheck", expected: "Profile verification failed: boom"), - ]) - func toolDescriptions(tool: String, expected: String) { - let error = ArgyllRunnerError.toolFailed(tool: tool, code: 1, logs: ["boom"]) - #expect(error.errorDescription == expected) + ] + for (tool, expected) in cases { + let error = ArgyllRunnerError.toolFailed(tool: tool, code: 1, logs: ["boom"]) + XCTAssertEqual(error.errorDescription, expected) + } } - @Test("toolFailed falls back to a generic description for unmapped tools and empty logs") - func genericFallbacks() { + func testGenericFallbacks() { let unknown = ArgyllRunnerError.toolFailed(tool: "targen", code: 7, logs: ["boom"]) - #expect(unknown.errorDescription == "Process exited with code 7") + XCTAssertEqual(unknown.errorDescription, "Process exited with code 7") let emptyLogs = ArgyllRunnerError.toolFailed(tool: "colprof", code: 2, logs: []) - #expect(emptyLogs.errorDescription - == "Profile creation failed: exited with code 2") + XCTAssertEqual(emptyLogs.errorDescription, + "Profile creation failed: exited with code 2") } } diff --git a/Tests/ICCeryCoreTests/CalibrationStoreTests.swift b/Tests/ICCeryCoreTests/CalibrationStoreTests.swift index 8b7ab56..509d71f 100644 --- a/Tests/ICCeryCoreTests/CalibrationStoreTests.swift +++ b/Tests/ICCeryCoreTests/CalibrationStoreTests.swift @@ -1,9 +1,8 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("CalibrationStore") -struct CalibrationStoreTests { +final class CalibrationStoreTests: XCTestCase { private static let sampleCal = """ CTI3 @@ -23,8 +22,7 @@ struct CalibrationStoreTests { END_DATA """ - @Test("Loads metadata and curves from .cal") - func parseCal() async throws { + func testParseCal() async throws { let url = FileManager.default.temporaryDirectory .appendingPathComponent("test_\(UUID().uuidString).cal") try Self.sampleCal.write(to: url, atomically: true, encoding: .utf8) @@ -33,17 +31,16 @@ struct CalibrationStoreTests { try await store.load(url: url) let data = await store.data - #expect(data?.colorRep == "RGB") - #expect(data?.descriptor == "Test printer") - #expect(data?.maxTac == 300) - #expect(data?.curves.count == 3) + XCTAssertEqual(data?.colorRep, "RGB") + XCTAssertEqual(data?.descriptor, "Test printer") + XCTAssertEqual(data?.maxTac, 300) + XCTAssertEqual(data?.curves.count, 3) let r = data?.curves.first { $0.channel == "R" } - #expect(r?.output == [0, 64, 255]) + XCTAssertEqual(r?.output, [0, 64, 255]) } - @Test("Staleness is true for a very old calibration") - func staleCalibration() async throws { + func testStaleCalibration() async throws { let url = FileManager.default.temporaryDirectory .appendingPathComponent("stale_\(UUID().uuidString).cal") try Self.sampleCal.write(to: url, atomically: true, encoding: .utf8) @@ -51,11 +48,10 @@ struct CalibrationStoreTests { let store = CalibrationStore(staleDays: 0) try await store.load(url: url) let stale = await store.isStale(comparedTo: "Other") - #expect(stale == true) + XCTAssertEqual(stale, true) } - @Test("Printer mismatch is flagged as stale") - func printerMismatch() async throws { + func testPrinterMismatch() async throws { let url = FileManager.default.temporaryDirectory .appendingPathComponent("mismatch_\(UUID().uuidString).cal") try Self.sampleCal.write(to: url, atomically: true, encoding: .utf8) @@ -64,6 +60,6 @@ struct CalibrationStoreTests { try await store.load(url: url) await store.setPrinterName("Printer A") let stale = await store.isStale(comparedTo: "Printer B") - #expect(stale == true) + XCTAssertEqual(stale, true) } } diff --git a/Tests/ICCeryCoreTests/JSONFileStoreTests.swift b/Tests/ICCeryCoreTests/JSONFileStoreTests.swift index c64bdaf..4d244d5 100644 --- a/Tests/ICCeryCoreTests/JSONFileStoreTests.swift +++ b/Tests/ICCeryCoreTests/JSONFileStoreTests.swift @@ -1,26 +1,23 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("JSONFileStore") -struct JSONFileStoreTests { +final class JSONFileStoreTests: XCTestCase { private func tempURL() -> URL { FileManager.default.temporaryDirectory .appendingPathComponent("json-store-\(UUID().uuidString).json") } - @Test("Missing file returns default") - func missingFileDefaults() throws { + func testMissingFileDefaults() throws { let store = JSONFileStore( fileURL: tempURL(), corrupt: .throwCorrupt, defaultValue: { .default } ) - #expect(try store.load() == .default) + XCTAssertEqual(try store.load(), .default) } - @Test("Corrupt file with replaceWithDefault returns default and leaves bytes") - func corruptDefaults() throws { + func testCorruptDefaults() throws { let url = tempURL() try "{ not json".write(to: url, atomically: true, encoding: .utf8) let store = JSONFileStore( @@ -28,13 +25,12 @@ struct JSONFileStoreTests { corrupt: .replaceWithDefault, defaultValue: { .default } ) - #expect(try store.load() == .default) + XCTAssertEqual(try store.load(), .default) let kept = try String(contentsOf: url, encoding: .utf8) - #expect(kept == "{ not json") + XCTAssertEqual(kept, "{ not json") } - @Test("Corrupt file with throwCorrupt throws and leaves bytes") - func corruptThrows() throws { + func testCorruptThrows() throws { let url = tempURL() try "not json".write(to: url, atomically: true, encoding: .utf8) let store = JSONFileStore<[Int]>( @@ -42,15 +38,12 @@ struct JSONFileStoreTests { corrupt: .throwCorrupt, defaultValue: { [] } ) - #expect(throws: DecodingError.self) { - _ = try store.load() - } + XCTAssertThrowsError(try store.load()) { error in XCTAssertTrue(error is DecodingError) } let kept = try String(contentsOf: url, encoding: .utf8) - #expect(kept == "not json") + XCTAssertEqual(kept, "not json") } - @Test("Pretty sorted keys") - func prettySorted() throws { + func testPrettySorted() throws { let url = tempURL() let store = JSONFileStore( fileURL: url, @@ -59,8 +52,8 @@ struct JSONFileStoreTests { ) try store.save(.default) let text = try String(contentsOf: url, encoding: .utf8) - #expect(text.contains("\n")) - #expect(text.contains("\"delta_e_good_max\"")) + XCTAssertTrue(text.contains("\n")) + XCTAssertTrue(text.contains("\"delta_e_good_max\"")) // Lexical key sorting: ascending order of top-level keys. let keys = [ "ask_before_overwrite_profile", @@ -75,7 +68,7 @@ struct JSONFileStoreTests { var lastIndex = text.startIndex for key in keys { guard let range = text.range(of: "\"\(key)\"", range: lastIndex.. URL { let dir = FileManager.default.temporaryDirectory @@ -119,58 +101,52 @@ struct PresetStoreTests { return dir.appendingPathComponent("settings.json") } - @Test("CRUD + export/import round-trip") - func crud() throws { + func testCrud() throws { let url = try tempSettingsURL() defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) } let store = PresetStore(settingsStore: SettingsStore(fileURL: url)) var p = ProfilingPreset(id: "custom-x", name: "Mine", patchCount: 999, dpi: 150) try store.saveCustom(p) - #expect(store.customs().count == 1) - #expect(store.all().count == 5) + XCTAssertEqual(store.customs().count, 1) + XCTAssertEqual(store.all().count, 5) p.name = "Renamed" try store.saveCustom(p) - #expect(store.customs().count == 1) - #expect(store.customs()[0].name == "Renamed") + XCTAssertEqual(store.customs().count, 1) + XCTAssertEqual(store.customs()[0].name, "Renamed") let data = try store.export(p) let imported = try store.import(data) - #expect(imported.name == "Renamed") - #expect(imported.dpi == 150) + XCTAssertEqual(imported.name, "Renamed") + XCTAssertEqual(imported.dpi, 150) - #expect(try store.deleteCustom(id: "custom-x")) - #expect(store.customs().isEmpty) - #expect(try !store.deleteCustom(id: "preset-std-rgb")) + XCTAssertTrue(try store.deleteCustom(id: "custom-x")) + XCTAssertTrue(store.customs().isEmpty) + XCTAssertFalse(try store.deleteCustom(id: "preset-std-rgb")) } - @Test("Import rewrites a built-in id to a fresh custom id") - func importBuiltinCollision() throws { + func testImportBuiltinCollision() throws { let url = try tempSettingsURL() defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) } let store = PresetStore(settingsStore: SettingsStore(fileURL: url)) let data = try store.export(PresetCatalog.standardRGB) let imported = try store.import(data) - #expect(imported.id.hasPrefix("custom-")) - #expect(!PresetCatalog.isBuiltIn(imported.id)) + XCTAssertTrue(imported.id.hasPrefix("custom-")) + XCTAssertFalse(PresetCatalog.isBuiltIn(imported.id)) } - @Test("Built-ins are immutable through saveCustom") - func builtInImmutable() throws { + func testBuiltInImmutable() throws { let url = try tempSettingsURL() defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) } let store = PresetStore(settingsStore: SettingsStore(fileURL: url)) var shadowed = PresetCatalog.standardRGB shadowed.name = "Hacked" - #expect(throws: PresetStore.PresetStoreError.self) { - try store.saveCustom(shadowed) - } + XCTAssertThrowsError(try store.saveCustom(shadowed)) { error in XCTAssertTrue(error is PresetStore.PresetStoreError) } } } -@Suite("AppSettings preset migration") -struct PresetMigrationTests { +final class PresetMigrationTests: XCTestCase { private func tempSettingsURL() throws -> URL { let dir = FileManager.default.temporaryDirectory @@ -179,8 +155,7 @@ struct PresetMigrationTests { return dir.appendingPathComponent("settings.json") } - @Test("Legacy M1 custom_presets migrate to typed schema") - func legacyMigration() throws { + func testLegacyMigration() throws { let url = try tempSettingsURL() defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) } let legacy = """ @@ -194,20 +169,19 @@ struct PresetMigrationTests { try legacy.write(to: url) let settings = SettingsStore(fileURL: url).load() - #expect(settings.customPresets.count == 1) + XCTAssertEqual(settings.customPresets.count, 1) let p = settings.customPresets[0] - #expect(p.name == "Old One") - #expect(p.id.hasPrefix("custom-0-")) - #expect(p.colourSpace == "cmyk") - #expect(p.patchCount == 900) - #expect(p.dpi == 150) - #expect(p.bitDepth == 16) - #expect(p.instrument == "p3") - #expect(p.pageSize == "A3") + XCTAssertEqual(p.name, "Old One") + XCTAssertTrue(p.id.hasPrefix("custom-0-")) + XCTAssertEqual(p.colourSpace, "cmyk") + XCTAssertEqual(p.patchCount, 900) + XCTAssertEqual(p.dpi, 150) + XCTAssertEqual(p.bitDepth, 16) + XCTAssertEqual(p.instrument, "p3") + XCTAssertEqual(p.pageSize, "A3") } - @Test("Typed presets load and re-save as the typed schema") - func typedRoundTrip() throws { + func testTypedRoundTrip() throws { let url = try tempSettingsURL() defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) } let store = SettingsStore(fileURL: url) @@ -215,50 +189,45 @@ struct PresetMigrationTests { s.customPresets = [ProfilingPreset(id: "c1", name: "C1", patchCount: 700)] try store.save(s) let loaded = store.load() - #expect(loaded.customPresets.first?.patchCount == 700) + XCTAssertEqual(loaded.customPresets.first?.patchCount, 700) } - @Test("Draft preset dpi=150 survives Codable + settings round-trip") - func draftDPI() throws { + func testDraftDPI() throws { let url = try tempSettingsURL() defer { try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) } let store = PresetStore(settingsStore: SettingsStore(fileURL: url)) let data = try store.export(PresetCatalog.draftRGB) let obj = try JSONSerialization.jsonObject(with: data) as! [String: Any] - #expect(obj["dpi"] as? Int == 150) + XCTAssertEqual(obj["dpi"] as? Int, 150) let back = try store.import(data) - #expect(back.dpi == 150) + XCTAssertEqual(back.dpi, 150) } } -@Suite("Preset mapping") -struct PresetMappingTests { - @Test("Draft 150 DPI maps into PrinttargConfig") - func draftDpi() { +final class PresetMappingTests: XCTestCase { + func testDraftDpi() { let cfg = PrinttargConfig( preset: PresetCatalog.draftRGB, basename: "t", workingDirectory: nil, calibrationFile: nil ) - #expect(cfg.dpi == 150) - #expect(cfg.layoutOrder == .deterministic) + XCTAssertEqual(cfg.dpi, 150) + XCTAssertEqual(cfg.layoutOrder, .deterministic) } - @Test("Nil optional targen fields stay nil") - func optionalNil() { + func testOptionalNil() { let preset = ProfilingPreset(id: "x", name: "n", patchCount: 800) let cfg = TargenConfig(preset: preset, basename: "t", workingDirectory: nil) - #expect(cfg.greySteps == nil) - #expect(cfg.singleChannelSteps == nil) - #expect(cfg.neutralSteps == nil) - #expect(cfg.totalInkLimit == nil) - #expect(cfg.darkEmphasis == nil) - #expect(cfg.devicePower == nil) + XCTAssertNil(cfg.greySteps) + XCTAssertNil(cfg.singleChannelSteps) + XCTAssertNil(cfg.neutralSteps) + XCTAssertNil(cfg.totalInkLimit) + XCTAssertNil(cfg.darkEmphasis) + XCTAssertNil(cfg.devicePower) } - @Test("Custom page and FWA survive a config round-trip") - func roundTripConfigs() { + func testRoundTripConfigs() { var preset = PresetCatalog.highQualityCMYK preset.pageSize = "210x297" preset.colprofFwa = "D50" @@ -268,9 +237,9 @@ struct PresetMappingTests { preset: preset, basename: "job", workingDirectory: nil, calibrationFile: nil ) let colprof = ColprofConfig(preset: preset, basename: "job", workingDirectory: nil) - #expect(printtarg.pageSize == .custom) - #expect(printtarg.customPageWidth == 210) - #expect(colprof.fwa == "D50") + XCTAssertEqual(printtarg.pageSize, .custom) + XCTAssertEqual(printtarg.customPageWidth, 210) + XCTAssertEqual(colprof.fwa, "D50") let back = ProfilingPreset( id: preset.id, name: preset.name, @@ -281,15 +250,14 @@ struct PresetMappingTests { calibrationFile: preset.calibrationFile, applyCalibration: preset.applyCalibration ) - #expect(back.dpi == preset.dpi) - #expect(back.colourSpace == "cmyk") - #expect(back.pageSize == "210x297") - #expect(back.colprofFwa == "D50") - #expect(back.greySteps == nil) + XCTAssertEqual(back.dpi, preset.dpi) + XCTAssertEqual(back.colourSpace, "cmyk") + XCTAssertEqual(back.pageSize, "210x297") + XCTAssertEqual(back.colprofFwa, "D50") + XCTAssertNil(back.greySteps) } - @Test("Full preset round-trips through all three configs with every field asserted") - func fullRoundTrip() { + func testFullRoundTrip() { let preset = ProfilingPreset( id: "custom-full", name: "Full", @@ -328,21 +296,21 @@ struct PresetMappingTests { ) let targen = TargenConfig(preset: preset, basename: "j", workingDirectory: nil) - #expect(targen.colourSpace == .cmyk) - #expect(targen.patchCount == 1500) - #expect(targen.whitePatches == 6) - #expect(targen.blackPatches == 8) - #expect(targen.greySteps == 9) - #expect(targen.singleChannelSteps == 7) - #expect(targen.neutralSteps == 4) - #expect(targen.neutralConcentration == 0.7) - #expect(targen.preconditioningProfile == "/tmp/pre.icm") - #expect(targen.ofpsHighQuality == true) - #expect(targen.ofpsAdaptation == 0.2) - #expect(targen.fullSpreadAlgorithm == .uniformRandom) - #expect(targen.totalInkLimit == 280) - #expect(targen.darkEmphasis == 1.3) - #expect(targen.devicePower == 1.2) + XCTAssertEqual(targen.colourSpace, .cmyk) + XCTAssertEqual(targen.patchCount, 1500) + XCTAssertEqual(targen.whitePatches, 6) + XCTAssertEqual(targen.blackPatches, 8) + XCTAssertEqual(targen.greySteps, 9) + XCTAssertEqual(targen.singleChannelSteps, 7) + XCTAssertEqual(targen.neutralSteps, 4) + XCTAssertEqual(targen.neutralConcentration, 0.7) + XCTAssertEqual(targen.preconditioningProfile, "/tmp/pre.icm") + XCTAssertEqual(targen.ofpsHighQuality, true) + XCTAssertEqual(targen.ofpsAdaptation, 0.2) + XCTAssertEqual(targen.fullSpreadAlgorithm, .uniformRandom) + XCTAssertEqual(targen.totalInkLimit, 280) + XCTAssertEqual(targen.darkEmphasis, 1.3) + XCTAssertEqual(targen.devicePower, 1.2) let printtarg = PrinttargConfig( preset: preset, @@ -350,25 +318,25 @@ struct PresetMappingTests { workingDirectory: nil, calibrationFile: preset.calibrationFile ) - #expect(printtarg.instrument == .p3) - #expect(printtarg.pageSize == .custom) - #expect(printtarg.customPageWidth == 250) - #expect(printtarg.customPageHeight == 300) - #expect(printtarg.bitDepth == .sixteen) - #expect(printtarg.dpi == 360) - #expect(printtarg.layoutOrder == .customSeed) - #expect(printtarg.customSeed == 42) - #expect(printtarg.calibrationFile == "/tmp/a.cal") + XCTAssertEqual(printtarg.instrument, .p3) + XCTAssertEqual(printtarg.pageSize, .custom) + XCTAssertEqual(printtarg.customPageWidth, 250) + XCTAssertEqual(printtarg.customPageHeight, 300) + XCTAssertEqual(printtarg.bitDepth, .sixteen) + XCTAssertEqual(printtarg.dpi, 360) + XCTAssertEqual(printtarg.layoutOrder, .customSeed) + XCTAssertEqual(printtarg.customSeed, 42) + XCTAssertEqual(printtarg.calibrationFile, "/tmp/a.cal") let colprof = ColprofConfig(preset: preset, basename: "j", workingDirectory: nil) - #expect(colprof.algorithm == "x") - #expect(colprof.quality == "u") - #expect(colprof.intent == "p") - #expect(colprof.fwa == "D65") - #expect(colprof.illuminant == "D65") - #expect(colprof.observer == "1931_2") - #expect(colprof.inputViewingCond == "D50_2") - #expect(colprof.outputViewingCond == "D65_2") + XCTAssertEqual(colprof.algorithm, "x") + XCTAssertEqual(colprof.quality, "u") + XCTAssertEqual(colprof.intent, "p") + XCTAssertEqual(colprof.fwa, "D65") + XCTAssertEqual(colprof.illuminant, "D65") + XCTAssertEqual(colprof.observer, "1931_2") + XCTAssertEqual(colprof.inputViewingCond, "D50_2") + XCTAssertEqual(colprof.outputViewingCond, "D65_2") let back = ProfilingPreset( id: preset.id, @@ -380,117 +348,126 @@ struct PresetMappingTests { calibrationFile: preset.calibrationFile, applyCalibration: preset.applyCalibration ) - #expect(back == preset) + XCTAssertEqual(back, preset) } - @Test("Every full-spread algorithm round-trips", arguments: [ - ("ofps", FullSpreadAlgorithm.ofps), - ("t", .target), - ("r", .random), - ("R", .uniformRandom), - ("q", .quasiRandom), - ("Q", .uniformQuasiRandom), - ("i", .invertedQuasiRandom), - ("I", .invertedUniformQuasiRandom) - ]) - func fullSpreadAlgorithms(value: String, expected: FullSpreadAlgorithm) { - var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100) - preset.fullSpreadAlgorithm = value - let cfg = TargenConfig(preset: preset, basename: "t", workingDirectory: nil) - if expected == .ofps { - // ofps is the default — no flag emitted, stored value is nil. - #expect(cfg.fullSpreadAlgorithm == nil) - } else { - #expect(cfg.fullSpreadAlgorithm == expected) + func testFullSpreadAlgorithms() { + let cases: [(String, FullSpreadAlgorithm)] = [ + ("ofps", .ofps), + ("t", .target), + ("r", .random), + ("R", .uniformRandom), + ("q", .quasiRandom), + ("Q", .uniformQuasiRandom), + ("i", .invertedQuasiRandom), + ("I", .invertedUniformQuasiRandom) + ] + for (value, expected) in cases { + var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100) + preset.fullSpreadAlgorithm = value + let cfg = TargenConfig(preset: preset, basename: "t", workingDirectory: nil) + if expected == .ofps { + // ofps is the default — no flag emitted, stored value is nil. + XCTAssertNil(cfg.fullSpreadAlgorithm) + } else { + XCTAssertEqual(cfg.fullSpreadAlgorithm, expected) + } + let back = ProfilingPreset( + id: "x", name: "n", description: "", + targen: cfg, + printtarg: PrinttargConfig( + preset: preset, basename: "t", + workingDirectory: nil, calibrationFile: nil + ), + colprof: ColprofConfig(preset: preset, basename: "t", workingDirectory: nil), + calibrationFile: nil, + applyCalibration: nil + ) + XCTAssertEqual(back.fullSpreadAlgorithm, value) } - let back = ProfilingPreset( - id: "x", name: "n", description: "", - targen: cfg, - printtarg: PrinttargConfig( - preset: preset, basename: "t", - workingDirectory: nil, calibrationFile: nil - ), - colprof: ColprofConfig(preset: preset, basename: "t", workingDirectory: nil), - calibrationFile: nil, - applyCalibration: nil - ) - #expect(back.fullSpreadAlgorithm == value) } - @Test("Explicit ofpsHighQuality=false is preserved, distinct from nil") - func ofpsHighQualityFalse() { + func testOfpsHighQualityFalse() { var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100) preset.ofpsHighQuality = false let cfg = TargenConfig(preset: preset, basename: "t", workingDirectory: nil) - #expect(cfg.ofpsHighQuality == false) + XCTAssertEqual(cfg.ofpsHighQuality, false) preset.ofpsHighQuality = nil let nilCfg = TargenConfig(preset: preset, basename: "t", workingDirectory: nil) - #expect(nilCfg.ofpsHighQuality == nil) + XCTAssertNil(nilCfg.ofpsHighQuality) } - @Test("noRandomize/seed layout mapping rules", arguments: [ - (true, nil, LayoutOrder.raster, 1), - (true, 7, .raster, 7), - (false, nil, .deterministic, 1), - (false, 1, .deterministic, 1), - (nil, 1, .deterministic, 1), - (false, 5, .customSeed, 5) - ] as [(Bool?, Int?, LayoutOrder, Int)]) - func layoutMapping(noRandomize: Bool?, seed: Int?, layout: LayoutOrder, expectedSeed: Int) { - var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100) - preset.noRandomize = noRandomize - preset.randomSeed = seed - let cfg = PrinttargConfig( - preset: preset, basename: "t", - workingDirectory: nil, calibrationFile: nil - ) - #expect(cfg.layoutOrder == layout) - #expect(cfg.customSeed == expectedSeed) + func testLayoutMapping() { + let cases: [(Bool?, Int?, LayoutOrder, Int)] = [ + (true, nil, .raster, 1), + (true, 7, .raster, 7), + (false, nil, .deterministic, 1), + (false, 1, .deterministic, 1), + (nil, 1, .deterministic, 1), + (false, 5, .customSeed, 5) + ] + for (noRandomize, seed, layout, expectedSeed) in cases { + var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100) + preset.noRandomize = noRandomize + preset.randomSeed = seed + let cfg = PrinttargConfig( + preset: preset, basename: "t", + workingDirectory: nil, calibrationFile: nil + ) + XCTAssertEqual(cfg.layoutOrder, layout) + XCTAssertEqual(cfg.customSeed, expectedSeed) + } } - @Test("Custom page fallback matrix", arguments: [ - ("250x300", PageSize.custom, 250.0, 300.0), - ("50x50", .custom, 50.0, 50.0), - ("foo", .a4, 210.0, 297.0), - ("30x40", .a4, 210.0, 297.0), - ("210x", .a4, 210.0, 297.0) - ] as [(String, PageSize, Double, Double)]) - func customPageFallback(raw: String, page: PageSize, w: Double, h: Double) { - var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100) - preset.pageSize = raw - let cfg = PrinttargConfig( - preset: preset, basename: "t", - workingDirectory: nil, calibrationFile: nil - ) - #expect(cfg.pageSize == page) - #expect(cfg.customPageWidth == w) - #expect(cfg.customPageHeight == h) + func testCustomPageFallback() { + let cases: [(String, PageSize, Double, Double)] = [ + ("250x300", .custom, 250.0, 300.0), + ("50x50", .custom, 50.0, 50.0), + ("foo", .a4, 210.0, 297.0), + ("30x40", .a4, 210.0, 297.0), + ("210x", .a4, 210.0, 297.0) + ] + for (raw, page, w, h) in cases { + var preset = ProfilingPreset(id: "x", name: "n", patchCount: 100) + preset.pageSize = raw + let cfg = PrinttargConfig( + preset: preset, basename: "t", + workingDirectory: nil, calibrationFile: nil + ) + XCTAssertEqual(cfg.pageSize, page) + XCTAssertEqual(cfg.customPageWidth, w) + XCTAssertEqual(cfg.customPageHeight, h) + } } - @Test("FWA preset value → selection matrix", arguments: [ - (nil, ColprofFwaSelection.none), - ("none", .none), - ("NONE", .none), - ("", .empty), - ("D50", .D50), - ("d50", .D50), - ("D65", .D65), - ("d65", .D65), - ("/tmp/fwa.sp", .custom) - ] as [(String?, ColprofFwaSelection)]) - func fwaToSelection(raw: String?, expected: ColprofFwaSelection) { - #expect(ColprofFwaSelection(presetValue: raw) == expected) + func testFwaToSelection() { + let cases: [(String?, ColprofFwaSelection)] = [ + (nil, .none), + ("none", .none), + ("NONE", .none), + ("", .empty), + ("D50", .D50), + ("d50", .D50), + ("D65", .D65), + ("d65", .D65), + ("/tmp/fwa.sp", .custom) + ] + for (raw, expected) in cases { + XCTAssertEqual(ColprofFwaSelection(presetValue: raw), expected) + } } - @Test("FWA selection → preset value matrix", arguments: [ - (ColprofFwaSelection.none, nil), - (.empty, ""), - (.D50, "D50"), - (.D65, "D65"), - (.custom, "/tmp/fwa.sp") - ] as [(ColprofFwaSelection, String?)]) - func fwaToPresetValue(selection: ColprofFwaSelection, expected: String?) { - #expect(selection.presetValue(customPath: "/tmp/fwa.sp") == expected) + func testFwaToPresetValue() { + let cases: [(ColprofFwaSelection, String?)] = [ + (.none, nil), + (.empty, ""), + (.D50, "D50"), + (.D65, "D65"), + (.custom, "/tmp/fwa.sp") + ] + for (selection, expected) in cases { + XCTAssertEqual(selection.presetValue(customPath: "/tmp/fwa.sp"), expected) + } } } diff --git a/Tests/ICCeryCoreTests/PresetViewModelMappingTests.swift b/Tests/ICCeryCoreTests/PresetViewModelMappingTests.swift index 30a96e4..08806f1 100644 --- a/Tests/ICCeryCoreTests/PresetViewModelMappingTests.swift +++ b/Tests/ICCeryCoreTests/PresetViewModelMappingTests.swift @@ -1,21 +1,19 @@ -import Testing import Foundation +import XCTest @testable import ICCeryCore @testable import ICCery /// Issue #82 — preset application through the live view models, under an /// isolated `TestAppEnvironment` (temp stores, fresh ProcessManager). -@Suite("PresetViewModelMapping") @MainActor -struct PresetViewModelMappingTests { +final class PresetViewModelMappingTests: XCTestCase { private func makeWorkflow() throws -> (TestAppEnvironment, TargetWorkflowViewModel) { let env = try TestAppEnvironment.make() return (env, TargetWorkflowViewModel(environment: env.environment)) } - @Test("Applying a nil-FWA preset after a custom FWA clears the stale path") - func nilFwaClearsCustomPath() throws { + func testNilFwaClearsCustomPath() throws { let (env, vm) = try makeWorkflow() defer { env.cleanup() } @@ -24,18 +22,17 @@ struct PresetViewModelMappingTests { colprofFwa: "/tmp/fwa.sp" ) vm.applyPreset(customPreset) - #expect(vm.profile.fwaSelection == .custom) - #expect(vm.profile.fwaCustomPath == "/tmp/fwa.sp") + XCTAssertEqual(vm.profile.fwaSelection, .custom) + XCTAssertEqual(vm.profile.fwaCustomPath, "/tmp/fwa.sp") customPreset.colprofFwa = nil vm.applyPreset(customPreset) - #expect(vm.profile.fwaSelection == .none) - #expect(vm.profile.fwaCustomPath == "") - #expect(vm.profile.fwaValue == nil) + XCTAssertEqual(vm.profile.fwaSelection, .none) + XCTAssertEqual(vm.profile.fwaCustomPath, "") + XCTAssertNil(vm.profile.fwaValue) } - @Test("Custom FWA preset path survives the round-trip to colprof_fwa") - func customFwaRoundTrip() throws { + func testCustomFwaRoundTrip() throws { let (env, vm) = try makeWorkflow() defer { env.cleanup() } @@ -44,13 +41,12 @@ struct PresetViewModelMappingTests { colprofFwa: "/tmp/other.sp" ) vm.applyPreset(preset) - #expect(vm.profile.fwaSelection == .custom) - #expect(vm.profile.fwaCustomPath == "/tmp/other.sp") - #expect(vm.profile.fwaValue == "/tmp/other.sp") + XCTAssertEqual(vm.profile.fwaSelection, .custom) + XCTAssertEqual(vm.profile.fwaCustomPath, "/tmp/other.sp") + XCTAssertEqual(vm.profile.fwaValue, "/tmp/other.sp") } - @Test("Preset calibration reaches Stage 2 instead of stale live state") - func presetCalibrationReachesStage2() throws { + func testPresetCalibrationReachesStage2() throws { let (env, vm) = try makeWorkflow() defer { env.cleanup() } @@ -65,13 +61,12 @@ struct PresetViewModelMappingTests { ) vm.applyPreset(preset) - #expect(vm.profile.applyCalibration) - #expect(vm.profile.calibrationFile == "/tmp/preset.cal") - #expect(vm.buildPrinttargConfig().calibrationFile == "/tmp/preset.cal") + XCTAssertTrue(vm.profile.applyCalibration) + XCTAssertEqual(vm.profile.calibrationFile, "/tmp/preset.cal") + XCTAssertEqual(vm.buildPrinttargConfig().calibrationFile, "/tmp/preset.cal") } - @Test("Preset with calibration disabled clears Stage 2 calibration") - func disabledCalibrationClearsStage2() throws { + func testDisabledCalibrationClearsStage2() throws { let (env, vm) = try makeWorkflow() defer { env.cleanup() } @@ -85,12 +80,11 @@ struct PresetViewModelMappingTests { ) vm.applyPreset(preset) - #expect(!vm.profile.applyCalibration) - #expect(vm.buildPrinttargConfig().calibrationFile == nil) + XCTAssertFalse(vm.profile.applyCalibration) + XCTAssertNil(vm.buildPrinttargConfig().calibrationFile) } - @Test("Preset Stage 1/2 form fields apply to the live form") - func formFieldsApply() throws { + func testFormFieldsApply() throws { let (env, vm) = try makeWorkflow() defer { env.cleanup() } @@ -106,22 +100,22 @@ struct PresetViewModelMappingTests { ) vm.applyPreset(preset) - #expect(vm.colourSpace == .cmyk) - #expect(vm.effectivePatchCount == 1500) - #expect(vm.whitePatches == 6) - #expect(vm.blackPatches == 8) - #expect(vm.greyStepsEnabled && vm.greySteps == 9) - #expect(vm.algorithm == .random) - #expect(vm.tiffDpi == 150) - #expect(vm.pageSize == .custom) - #expect(vm.customPageW == 250 && vm.customPageH == 300) - #expect(vm.selectedPresetID == "c-form") + XCTAssertEqual(vm.colourSpace, .cmyk) + XCTAssertEqual(vm.effectivePatchCount, 1500) + XCTAssertEqual(vm.whitePatches, 6) + XCTAssertEqual(vm.blackPatches, 8) + XCTAssertTrue(vm.greyStepsEnabled && vm.greySteps == 9) + XCTAssertEqual(vm.algorithm, .random) + XCTAssertEqual(vm.tiffDpi, 150) + XCTAssertEqual(vm.pageSize, .custom) + XCTAssertTrue(vm.customPageW == 250 && vm.customPageH == 300) + XCTAssertEqual(vm.selectedPresetID, "c-form") // Disabled advanced controls stay nil in the snapshot, not // numeric sentinels. preset.greySteps = nil vm.applyPreset(preset) - #expect(!vm.greyStepsEnabled) - #expect(vm.buildTargenConfig().greySteps == nil) + XCTAssertFalse(vm.greyStepsEnabled) + XCTAssertNil(vm.buildTargenConfig().greySteps) } } diff --git a/Tests/ICCeryCoreTests/PrintPanelTests.swift b/Tests/ICCeryCoreTests/PrintPanelTests.swift index 3bdc0f2..5a92936 100644 --- a/Tests/ICCeryCoreTests/PrintPanelTests.swift +++ b/Tests/ICCeryCoreTests/PrintPanelTests.swift @@ -1,13 +1,12 @@ -import Testing import Foundation +import XCTest @testable import ICCeryCore @testable import ICCery /// Issue 13 — panel outcome mapping (cancel → nil, ok → result). /// The real `NSPrintPanel` is never run in tests; these exercise the /// `UITestHooks` seam the UI tests rely on. -@Suite("PrintPanelStub") -struct PrintPanelStubTests { +final class PrintPanelStubTests: XCTestCase { private func withEnv( _ vars: [String: String?], @@ -28,19 +27,17 @@ struct PrintPanelStubTests { try body() } - @Test("Cancel returns nil — not an error") - func cancelIsNil() throws { + func testCancelIsNil() throws { try withEnv([ "ICCERY_UI_TESTING": "1", "ICCERY_TEST_PRINT_PANEL": "cancel", ]) { - #expect(UITestHooks.printPanelStubbed) - #expect(UITestHooks.printPanelResult(forQueue: "q") == nil) + XCTAssertTrue(UITestHooks.printPanelStubbed) + XCTAssertNil(UITestHooks.printPanelResult(forQueue: "q")) } } - @Test("OK returns captured options + selected printer") - func okResult() throws { + func testOkResult() throws { try withEnv([ "ICCERY_UI_TESTING": "1", "ICCERY_TEST_PRINT_PANEL": "ok", @@ -48,15 +45,14 @@ struct PrintPanelStubTests { "ICCERY_TEST_PANEL_PRINTER": "Other_Queue", ]) { let result = UITestHooks.printPanelResult(forQueue: "q") - #expect(result?.selectedPrinter == "Other_Queue") - #expect(result?.options.cupsOptions == "MediaType=Photo InputSlot=Rear") - #expect(result?.options.mediaType == "Photo") - #expect(result?.options.ppdUncorrectedPassthrough == true) + XCTAssertEqual(result?.selectedPrinter, "Other_Queue") + XCTAssertEqual(result?.options.cupsOptions, "MediaType=Photo InputSlot=Rear") + XCTAssertEqual(result?.options.mediaType, "Photo") + XCTAssertEqual(result?.options.ppdUncorrectedPassthrough, true) } } - @Test("OK defaults selected printer to the opened queue") - func okDefaultsPrinter() throws { + func testOkDefaultsPrinter() throws { try withEnv([ "ICCERY_UI_TESTING": "1", "ICCERY_TEST_PRINT_PANEL": "ok", @@ -64,8 +60,8 @@ struct PrintPanelStubTests { "ICCERY_TEST_PANEL_PRINTER": nil, ]) { let result = UITestHooks.printPanelResult(forQueue: "My_Queue") - #expect(result?.selectedPrinter == "My_Queue") - #expect(result?.options.cupsOptions == nil) + XCTAssertEqual(result?.selectedPrinter, "My_Queue") + XCTAssertNil(result?.options.cupsOptions) } } } diff --git a/Tests/ICCeryCoreTests/PrinttargTests.swift b/Tests/ICCeryCoreTests/PrinttargTests.swift index b147afb..a64df4c 100644 --- a/Tests/ICCeryCoreTests/PrinttargTests.swift +++ b/Tests/ICCeryCoreTests/PrinttargTests.swift @@ -1,4 +1,3 @@ -import Testing import XCTest import Foundation @testable import ICCeryCore @@ -257,8 +256,7 @@ final class PrinttargManifestTests: XCTestCase { } } -@Suite("ArgyllRunner Printtarg") -struct ArgyllRunnerPrinttargTests { +final class ArgyllRunnerPrinttargTests: XCTestCase { private func makeFixture(_ body: String, name: String = "printtarg") throws -> URL { let dir = FileManager.default.temporaryDirectory @@ -310,8 +308,7 @@ struct ArgyllRunnerPrinttargTests { try Data(bytes).write(to: url) } - @Test("Successful printtarg emits .ti2 + manifest + PNG previews") - func success() async throws { + func testSuccess() async throws { let dir = try makeFixture(""" #!/bin/sh last="" @@ -330,18 +327,17 @@ struct ArgyllRunnerPrinttargTests { processManager: ProcessManager(), binaryResolver: resolver) let config = PrinttargConfig(basename: "pt", workingDirectory: dir) let result = try await runner.runPrinttarg(config: config) - #expect(result.ti2URL.lastPathComponent == "pt.ti2") - #expect(result.manifest.pages.count == 1) - #expect(result.pages.count == 1) + XCTAssertEqual(result.ti2URL.lastPathComponent, "pt.ti2") + XCTAssertEqual(result.manifest.pages.count, 1) + XCTAssertEqual(result.pages.count, 1) let png = result.pages[0].previewPNG - #expect(png != nil) + XCTAssertNotNil(png) if let png { - #expect(png.prefix(8) == Data([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A])) + XCTAssertEqual(png.prefix(8), Data([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A])) } } - @Test("Non-zero exit throws toolFailed and stays on stage") - func failure() async throws { + func testFailure() async throws { let dir = try makeFixture(""" #!/bin/sh echo "oops" >&2 @@ -351,15 +347,16 @@ struct ArgyllRunnerPrinttargTests { let runner = ArgyllRunner( processManager: ProcessManager(), binaryResolver: BinaryResolver(bundledRoot: dir, overrideDir: dir)) - await #expect(throws: ArgyllRunnerError.toolFailed( - tool: "printtarg", code: 3, logs: ["oops"])) { + await assertAsyncThrows(expectedType: ArgyllRunnerError.self) { try await runner.runPrinttarg( config: PrinttargConfig(basename: "x", workingDirectory: dir)) + } errorHandler: { error in + XCTAssertEqual(error, .toolFailed( + tool: "printtarg", code: 3, logs: ["oops"])) } } - @Test("Exit 0 without manifest → malformedManifest") - func noManifest() async throws { + func testNoManifest() async throws { let dir = try makeFixture(""" #!/bin/sh last="" @@ -372,14 +369,13 @@ struct ArgyllRunnerPrinttargTests { let runner = ArgyllRunner( processManager: ProcessManager(), binaryResolver: BinaryResolver(bundledRoot: dir, overrideDir: dir)) - await #expect(throws: ArgyllRunnerError.self) { + await assertAsyncThrows(expectedType: ArgyllRunnerError.self) { try await runner.runPrinttarg( config: PrinttargConfig(basename: "x", workingDirectory: dir)) } } - @Test("Exit 0 without .ti2 → missingArtefact") - func noTi2() async throws { + func testNoTi2() async throws { let dir = try makeFixture(""" #!/bin/sh printf '{\\n"event":"manifest",\\n"pages":[]\\n}\\n' @@ -389,14 +385,13 @@ struct ArgyllRunnerPrinttargTests { let runner = ArgyllRunner( processManager: ProcessManager(), binaryResolver: BinaryResolver(bundledRoot: dir, overrideDir: dir)) - await #expect(throws: ArgyllRunnerError.self) { + await assertAsyncThrows(expectedType: ArgyllRunnerError.self) { try await runner.runPrinttarg( config: PrinttargConfig(basename: "x", workingDirectory: dir)) } } - @Test("Deterministic config produces byte-identical .ti2") - func determinism() async throws { + func testDeterminism() async throws { let dir = try makeFixture(""" #!/bin/sh last="" @@ -416,6 +411,6 @@ struct ArgyllRunnerPrinttargTests { config: PrinttargConfig(basename: "b", workingDirectory: dir)) let d1 = try Data(contentsOf: dir.appendingPathComponent("a.ti2")) let d2 = try Data(contentsOf: dir.appendingPathComponent("b.ti2")) - #expect(d1 == d2) + XCTAssertEqual(d1, d2) } } diff --git a/Tests/ICCeryCoreTests/ProcessManagerTests.swift b/Tests/ICCeryCoreTests/ProcessManagerTests.swift index d3a3f3a..261b5a9 100644 --- a/Tests/ICCeryCoreTests/ProcessManagerTests.swift +++ b/Tests/ICCeryCoreTests/ProcessManagerTests.swift @@ -1,12 +1,11 @@ -import Testing import XCTest import Foundation @testable import ICCeryCore /// Helpers shared across ProcessManager tests. Fixture binaries are shell /// scripts written to a temp dir — no resource bundling required. -@Suite("ProcessManager", .serialized) -struct ProcessManagerTests { +/// XCTest executes test methods serially by default. +final class ProcessManagerTests: XCTestCase { // MARK: - Fixture plumbing @@ -44,7 +43,7 @@ struct ProcessManagerTests { if box.finish() { cont.resume(returning: box.events) } } Task { - try? await Task.sleep(for: .seconds(timeout)) + try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) if box.finish() { cont.resume(returning: box.events) } } } @@ -85,7 +84,7 @@ struct ProcessManagerTests { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { if exitCount(in: box) > 0 { return true } - try? await Task.sleep(for: .milliseconds(10)) + try? await Task.sleep(nanoseconds: 10_000_000) } return false } @@ -94,7 +93,7 @@ struct ProcessManagerTests { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { if FileManager.default.fileExists(atPath: url.path) { return true } - try? await Task.sleep(for: .milliseconds(10)) + try? await Task.sleep(nanoseconds: 10_000_000) } return false } @@ -107,14 +106,14 @@ struct ProcessManagerTests { let deadline = Date().addingTimeInterval(timeout) while Date() < deadline { if await manager.isRunning(id) { return true } - try? await Task.sleep(for: .milliseconds(10)) + try? await Task.sleep(nanoseconds: 10_000_000) } return false } // MARK: - Tests - @Test func streamsStdoutAndEmitsExit() async throws { + func testStreamsStdoutAndEmitsExit() async throws { let pm = ProcessManager() let bin = try script("lines.sh", "#!/bin/sh\necho hello\necho world\n") async let events = collect(pm, id: "t1") @@ -123,21 +122,21 @@ struct ProcessManagerTests { let lines = evs.compactMap { e -> String? in if case .stdout(_, let l) = e { return l }; return nil } - #expect(lines == ["hello", "world"]) - #expect(evs.contains(.exit(id: "t1", code: 0))) + XCTAssertEqual(lines, ["hello", "world"]) + XCTAssertTrue(evs.contains(.exit(id: "t1", code: 0))) } - @Test func routesStderrSeparately() async throws { + func testRoutesStderrSeparately() async throws { let pm = ProcessManager() let bin = try script("err.sh", "#!/bin/sh\necho out\necho oops 1>&2\n") async let evs = collect(pm, id: "t2") try await pm.runStreaming(id: "t2", binary: bin, arguments: []) let events = await evs - #expect(events.contains(.stdout(id: "t2", line: "out"))) - #expect(events.contains(.stderr(id: "t2", line: "oops"))) + XCTAssertTrue(events.contains(.stdout(id: "t2", line: "out"))) + XCTAssertTrue(events.contains(.stderr(id: "t2", line: "oops"))) } - @Test func stripsRowColorsJSONPrefix() async throws { + func testStripsRowColorsJSONPrefix() async throws { let pm = ProcessManager() let bin = try script( "rows.sh", @@ -150,21 +149,22 @@ struct ProcessManagerTests { if case .jsonRow(_, let d) = e { return String(decoding: d, as: UTF8.self) } return nil } - #expect(rows == ["{\"row\":1}"]) - #expect(events.contains(.stdout(id: "t3", line: "plain"))) + XCTAssertEqual(rows, ["{\"row\":1}"]) + XCTAssertTrue(events.contains(.stdout(id: "t3", line: "plain"))) // Prefixed lines must not leak into stdout. - #expect(!events.contains(.stdout(id: "t3", line: "ROW_COLORS_JSON: {\"row\":1}"))) + XCTAssertFalse(events.contains(.stdout(id: "t3", line: "ROW_COLORS_JSON: {\"row\":1}"))) } - @Test func unterminatedTailFlushesOnExit() async throws { + func testUnterminatedTailFlushesOnExit() async throws { let pm = ProcessManager() let bin = try script("tail.sh", "#!/bin/sh\nprintf 'no-newline'\n") async let evs = collect(pm, id: "t4") try await pm.runStreaming(id: "t4", binary: bin, arguments: []) - #expect(await evs.contains(.stdout(id: "t4", line: "no-newline"))) + let t4SawTail = await evs.contains(.stdout(id: "t4", line: "no-newline")) + XCTAssertTrue(t4SawTail) } - @Test func stdinRoundTrip() async throws { + func testStdinRoundTrip() async throws { let pm = ProcessManager() // Read two lines then exit naturally — a killed sh would lose its // buffered stdio output, which is exactly the chartread pattern. @@ -177,21 +177,23 @@ struct ProcessManagerTests { try await pm.sendStdin(id: "t5", text: " \n") try await pm.sendStdin(id: "t5", text: "d\n") let events = await evs - #expect(events.contains(.stdout(id: "t5", line: "got: "))) - #expect(events.contains(.stdout(id: "t5", line: "got:d"))) + XCTAssertTrue(events.contains(.stdout(id: "t5", line: "got: "))) + XCTAssertTrue(events.contains(.stdout(id: "t5", line: "got:d"))) } - @Test func duplicateIDRejected() async throws { + func testDuplicateIDRejected() async throws { let pm = ProcessManager() let bin = try script("slow.sh", "#!/bin/sh\nsleep 30\n") try await pm.runStreaming(id: "t6", binary: bin, arguments: []) - await #expect(throws: ProcessError.duplicateID("t6")) { + await assertAsyncThrows(expectedType: ProcessError.self) { try await pm.runStreaming(id: "t6", binary: bin, arguments: []) + } errorHandler: { error in + XCTAssertEqual(error, .duplicateID("t6")) } await pm.kill(id: "t6") } - @Test func killEmitsExitAndClosesStdin() async throws { + func testKillEmitsExitAndClosesStdin() async throws { let pm = ProcessManager() let bin = try script("slow2.sh", "#!/bin/sh\ncat\n") async let evs = collect(pm, id: "t7") @@ -200,49 +202,51 @@ struct ProcessManagerTests { let events = await evs // exit emitted exactly once let exits = events.filter { if case .exit = $0 { return true }; return false } - #expect(exits.count == 1) - await #expect(throws: ProcessError.unknownID("t7")) { + XCTAssertEqual(exits.count, 1) + await assertAsyncThrows(expectedType: ProcessError.self) { try await pm.sendStdin(id: "t7", text: "d\n") + } errorHandler: { error in + XCTAssertEqual(error, .unknownID("t7")) } } - @Test func killAllCountsSignaled() async throws { + func testKillAllCountsSignaled() async throws { let pm = ProcessManager() let bin = try script("slow3.sh", "#!/bin/sh\nsleep 30\n") try await pm.runStreaming(id: "a", binary: bin, arguments: []) try await pm.runStreaming(id: "b", binary: bin, arguments: []) let count = await pm.killAll() - #expect(count == 2) + XCTAssertEqual(count, 2) } - @Test func capturedRunReturnsBothStreams() async throws { + func testCapturedRunReturnsBothStreams() async throws { let pm = ProcessManager() let bin = try script("cap.sh", "#!/bin/sh\necho out-data\necho err-data 1>&2\nexit 3\n") let result = try await pm.runCaptured(id: "cap", binary: bin, arguments: []) - #expect(result.stdout.contains("out-data")) - #expect(result.stderr.contains("err-data")) - #expect(result.exitCode == 3) + XCTAssertTrue(result.stdout.contains("out-data")) + XCTAssertTrue(result.stderr.contains("err-data")) + XCTAssertEqual(result.exitCode, 3) } - @Test func capturedRunFastExit() async throws { + func testCapturedRunFastExit() async throws { let pm = ProcessManager() let bin = try script("fast.sh", "#!/bin/sh\nexit 7\n") let result = try await pm.runCaptured(id: "fast", binary: bin, arguments: []) - #expect(result.exitCode == 7) - #expect(result.stdout == "") - #expect(result.stderr == "") + XCTAssertEqual(result.exitCode, 7) + XCTAssertEqual(result.stdout, "") + XCTAssertEqual(result.stderr, "") } - @Test func capturedRunStderrOnly() async throws { + func testCapturedRunStderrOnly() async throws { let pm = ProcessManager() let bin = try script("stderr-only.sh", "#!/bin/sh\necho 'mock lp failure' 1>&2\nexit 1\n") let result = try await pm.runCaptured(id: "stderr-only", binary: bin, arguments: []) - #expect(result.exitCode == 1) - #expect(result.stdout == "") - #expect(result.stderr.contains("mock lp failure")) + XCTAssertEqual(result.exitCode, 1) + XCTAssertEqual(result.stdout, "") + XCTAssertTrue(result.stderr.contains("mock lp failure")) } - @Test func capturedRunDoesNotDeadlockOnLargeOutput() async throws { + func testCapturedRunDoesNotDeadlockOnLargeOutput() async throws { let pm = ProcessManager() // 5000 lines each stream exceeds the 64 KiB pipe buffer. let bin = try script( @@ -250,26 +254,29 @@ struct ProcessManagerTests { "#!/bin/sh\ni=0; while [ $i -lt 5000 ]; do echo \"out-$i\"; echo \"err-$i\" 1>&2; i=$((i+1)); done\n" ) let result = try await pm.runCaptured(id: "big", binary: bin, arguments: []) - #expect(result.stdout.contains("out-4999")) - #expect(result.stderr.contains("err-4999")) + XCTAssertTrue(result.stdout.contains("out-4999")) + XCTAssertTrue(result.stderr.contains("err-4999")) } - @Test func argyllEnvVarIsSet() async throws { + func testArgyllEnvVarIsSet() async throws { let pm = ProcessManager() let bin = try script("env.sh", "#!/bin/sh\necho \"ANI=$ARGYLL_NOT_INTERACTIVE\"\n") async let evs = collect(pm, id: "t10") try await pm.runStreaming(id: "t10", binary: bin, arguments: []) - #expect(await evs.contains(.stdout(id: "t10", line: "ANI=1"))) + let t10SawEnv = await evs.contains(.stdout(id: "t10", line: "ANI=1")) + XCTAssertTrue(t10SawEnv) } - @Test func unknownIDStdinThrows() async throws { + func testUnknownIDStdinThrows() async throws { let pm = ProcessManager() - await #expect(throws: ProcessError.unknownID("nope")) { + await assertAsyncThrows(expectedType: ProcessError.self) { try await pm.sendStdin(id: "nope", text: "d\n") + } errorHandler: { error in + XCTAssertEqual(error, .unknownID("nope")) } } - @Test func explicitPartialFlushEmitsRowColorsJSON() async throws { + func testExplicitPartialFlushEmitsRowColorsJSON() async throws { let pm = ProcessManager() let marker = Self.fixtureDir .appendingPathComponent("partial-row-ready-\(UUID().uuidString)") @@ -280,7 +287,8 @@ struct ProcessManagerTests { let box = Box() let observer = observe(pm, id: "t11", into: box) try await pm.runStreaming(id: "t11", binary: bin, arguments: [marker.path]) - #expect(await waitForFile(marker)) + let markerReady = await waitForFile(marker) + XCTAssertTrue(markerReady) // Retry the flush so the pipe-ingest task can win the actor race // on a loaded host; the first successful flush emits the row. var flushed = false @@ -290,24 +298,25 @@ struct ProcessManagerTests { flushed = true break } - try await Task.sleep(for: .milliseconds(20)) + try await Task.sleep(nanoseconds: 20_000_000) } - #expect(flushed) + XCTAssertTrue(flushed) await pm.kill(id: "t11") - #expect(await waitForExit(in: box)) + let sawExit = await waitForExit(in: box) + XCTAssertTrue(sawExit) observer.cancel() let events = box.events let rows = events.compactMap { e -> String? in if case .jsonRow(_, let d) = e { return String(decoding: d, as: UTF8.self) } return nil } - #expect(rows == ["{\"row\":9}"]) + XCTAssertEqual(rows, ["{\"row\":9}"]) // Prefixed tails must not leak into stdout, even via finalize. - #expect(!events.contains(.stdout(id: "t11", line: "ROW_COLORS_JSON: {\"row\":9}"))) - #expect(exitCount(in: box) == 1) + XCTAssertFalse(events.contains(.stdout(id: "t11", line: "ROW_COLORS_JSON: {\"row\":9}"))) + XCTAssertEqual(exitCount(in: box), 1) } - @Test func unterminatedRowTailFinalizesAsJSONRow() async throws { + func testUnterminatedRowTailFinalizesAsJSONRow() async throws { let pm = ProcessManager() let bin = try script( "row-tail.sh", @@ -316,68 +325,70 @@ struct ProcessManagerTests { let box = Box() let observer = observe(pm, id: "t12", into: box) try await pm.runStreaming(id: "t12", binary: bin, arguments: []) - #expect(await waitForExit(in: box)) + let sawExit = await waitForExit(in: box) + XCTAssertTrue(sawExit) observer.cancel() let events = box.events let rows = events.compactMap { e -> String? in if case .jsonRow(_, let d) = e { return String(decoding: d, as: UTF8.self) } return nil } - #expect(rows == ["{\"row\":42}"]) - #expect(!events.contains(.stdout(id: "t12", line: "ROW_COLORS_JSON: {\"row\":42}"))) + XCTAssertEqual(rows, ["{\"row\":42}"]) + XCTAssertFalse(events.contains(.stdout(id: "t12", line: "ROW_COLORS_JSON: {\"row\":42}"))) let rowIndex = events.firstIndex { if case .jsonRow = $0 { return true }; return false } let exitIndexes = events.indices.filter { if case .exit = events[$0] { return true }; return false } - #expect(exitIndexes.count == 1) + XCTAssertEqual(exitIndexes.count, 1) if let rowIndex, let exitIndex = exitIndexes.first { - #expect(rowIndex < exitIndex) + XCTAssertTrue(rowIndex < exitIndex) } else { - Issue.record("expected a jsonRow before the exit event") + XCTFail("expected a jsonRow before the exit event") } } - @Test func fastStreamingExitEmitsExactlyOneExit() async throws { + func testFastStreamingExitEmitsExactlyOneExit() async throws { let pm = ProcessManager() let bin = try script("fast-stream.sh", "#!/bin/sh\nexit 0\n") let box = Box() let observer = observe(pm, id: "t13", into: box) try await pm.runStreaming(id: "t13", binary: bin, arguments: []) - #expect(await waitForExit(in: box)) + let sawExit = await waitForExit(in: box) + XCTAssertTrue(sawExit) // The grace window must outlast the 2 s finalize watchdog so a // duplicate emission from it would be observed. - try await Task.sleep(for: .milliseconds(2500)) + try await Task.sleep(nanoseconds: 2_500_000_000) observer.cancel() - #expect(box.events == [.exit(id: "t13", code: 0)]) + XCTAssertEqual(box.events, [.exit(id: "t13", code: 0)]) } - @Test func fastCapturedExitEmitsExactlyOneExit() async throws { + func testFastCapturedExitEmitsExactlyOneExit() async throws { let pm = ProcessManager() let bin = try script("fast-cap.sh", "#!/bin/sh\nexit 7\n") let box = Box() let observer = observe(pm, id: "t14", into: box) let result = try await pm.runCaptured(id: "t14", binary: bin, arguments: []) - #expect(result.exitCode == 7) + XCTAssertEqual(result.exitCode, 7) // Both the termination handler and the waitUntilExit watchdog // resume the same box; give the slower path time to fire. - try await Task.sleep(for: .milliseconds(500)) + try await Task.sleep(nanoseconds: 500_000_000) observer.cancel() - #expect(box.events == [.exit(id: "t14", code: 7)]) + XCTAssertEqual(box.events, [.exit(id: "t14", code: 7)]) } - @Test func capturedRunSetsArgyllNotInteractive() async throws { + func testCapturedRunSetsArgyllNotInteractive() async throws { let pm = ProcessManager() let bin = try script( "cap-env.sh", "#!/bin/sh\necho \"ANI=$ARGYLL_NOT_INTERACTIVE\"\n" ) let result = try await pm.runCaptured(id: "t15", binary: bin, arguments: []) - #expect(result.stdout == "ANI=1\n") + XCTAssertEqual(result.stdout, "ANI=1\n") } - @Test func killAllTerminatesStreamingAndCapturedChildren() async throws { + func testKillAllTerminatesStreamingAndCapturedChildren() async throws { let pm = ProcessManager() let marker = Self.fixtureDir .appendingPathComponent("mixed-cap-ready-\(UUID().uuidString)") @@ -391,21 +402,29 @@ struct ProcessManagerTests { let capTask = Task { try await pm.runCaptured(id: "t17", binary: capBin, arguments: [marker.path]) } - #expect(await waitForFile(marker)) - #expect(await waitForRunning(pm, id: "t16")) - #expect(await waitForRunning(pm, id: "t17")) - #expect(await pm.killAll() == 2) + let markerReady = await waitForFile(marker) + XCTAssertTrue(markerReady) + let t16Running = await waitForRunning(pm, id: "t16") + XCTAssertTrue(t16Running) + let t17Running = await waitForRunning(pm, id: "t17") + XCTAssertTrue(t17Running) + let killed = await pm.killAll() + XCTAssertEqual(killed, 2) _ = try await capTask.value - #expect(await waitForExit(in: streamBox)) - #expect(await waitForExit(in: capBox)) + let streamExit = await waitForExit(in: streamBox) + XCTAssertTrue(streamExit) + let capExit = await waitForExit(in: capBox) + XCTAssertTrue(capExit) // Grace window outlasts the streaming finalize watchdog. - try await Task.sleep(for: .milliseconds(2500)) + try await Task.sleep(nanoseconds: 2_500_000_000) streamObserver.cancel() capObserver.cancel() - #expect(!(await pm.isRunning("t16"))) - #expect(!(await pm.isRunning("t17"))) - #expect(exitCount(in: streamBox) == 1) - #expect(exitCount(in: capBox) == 1) + let t16RunningAfter = await pm.isRunning("t16") + XCTAssertFalse(t16RunningAfter) + let t17RunningAfter = await pm.isRunning("t17") + XCTAssertFalse(t17RunningAfter) + XCTAssertEqual(exitCount(in: streamBox), 1) + XCTAssertEqual(exitCount(in: capBox), 1) } } diff --git a/Tests/ICCeryCoreTests/ProcessRunSupportTests.swift b/Tests/ICCeryCoreTests/ProcessRunSupportTests.swift index 79ebe1d..a1edb9b 100644 --- a/Tests/ICCeryCoreTests/ProcessRunSupportTests.swift +++ b/Tests/ICCeryCoreTests/ProcessRunSupportTests.swift @@ -1,5 +1,5 @@ import Foundation -import Testing +import XCTest @testable import ICCery /// Direct contracts for the shared logged-run helper (issue #80). @@ -7,14 +7,12 @@ import Testing /// `runLogged` owns the running-flag transition (`false → true → false`) /// and the log-reset decision; these tests pin both sides of the /// contract plus the coalesced `@MainActor` log hop. -@Suite("ProcessRunSupport runLogged") @MainActor -struct ProcessRunSupportTests { +final class ProcessRunSupportTests: XCTestCase { private struct SentinelError: Error {} - @Test("Success: running transitions [true, false], log resets once, batches reach the main actor, value preserved") - func successTransitions() async throws { + func testSuccessTransitions() async throws { var running: [Bool] = [] var resets = 0 var received: [String] = [] @@ -31,20 +29,19 @@ struct ProcessRunSupportTests { return 42 } - #expect(result == 42) - #expect(running == [true, false]) - #expect(resets == 1) + XCTAssertEqual(result, 42) + XCTAssertEqual(running, [true, false]) + XCTAssertEqual(resets, 1) // The sink hops back through a main-actor Task; yield until the // coalesced batch lands. for _ in 0..<200 where received.isEmpty { - try await Task.sleep(for: .milliseconds(10)) + try await Task.sleep(nanoseconds: 10_000_000) } - #expect(received == ["alpha", "beta"]) + XCTAssertEqual(received, ["alpha", "beta"]) } - @Test("Failure: running still transitions [true, false], log resets once, error is rethrown") - func failureTransitions() async throws { + func testFailureTransitions() async throws { var running: [Bool] = [] var resets = 0 @@ -56,12 +53,12 @@ struct ProcessRunSupportTests { ) { _ -> Int in throw SentinelError() } - Issue.record("Expected runLogged to rethrow") + XCTFail("Expected runLogged to rethrow") } catch is SentinelError { // Expected path. } - #expect(running == [true, false]) - #expect(resets == 1) + XCTAssertEqual(running, [true, false]) + XCTAssertEqual(resets, 1) } } diff --git a/Tests/ICCeryCoreTests/ProfileInstallerTests.swift b/Tests/ICCeryCoreTests/ProfileInstallerTests.swift index 7851582..67cb353 100644 --- a/Tests/ICCeryCoreTests/ProfileInstallerTests.swift +++ b/Tests/ICCeryCoreTests/ProfileInstallerTests.swift @@ -1,5 +1,5 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore /// A `FileManager` subclass that reports a temporary directory as the @@ -18,8 +18,7 @@ private final class TestFileManager: FileManager { } } -@Suite("ProfileInstaller") -struct ProfileInstallerTests { +final class ProfileInstallerTests: XCTestCase { private func makeTempDir() throws -> URL { let fm = FileManager.default @@ -39,8 +38,7 @@ struct ProfileInstallerTests { return url } - @Test("Installs .icc to user ColorSync folder") - func userInstall() throws { + func testUserInstall() throws { let fm = FileManager.default let tmp = try makeTempDir() let testFM = TestFileManager(home: tmp) @@ -51,15 +49,14 @@ struct ProfileInstallerTests { fileManager: testFM ) - #expect(result.registered) - #expect(!result.overwritten) - #expect(!result.renamed) - #expect(result.destPath.hasSuffix("test.icc")) - #expect(fm.fileExists(atPath: result.destPath)) + XCTAssertTrue(result.registered) + XCTAssertFalse(result.overwritten) + XCTAssertFalse(result.renamed) + XCTAssertTrue(result.destPath.hasSuffix("test.icc")) + XCTAssertTrue(fm.fileExists(atPath: result.destPath)) } - @Test("Overwrite succeeds and replaces the existing file") - func overwriteSucceeds() throws { + func testOverwriteSucceeds() throws { let fm = FileManager.default let tmp = try makeTempDir() let testFM = TestFileManager(home: tmp) @@ -70,7 +67,7 @@ struct ProfileInstallerTests { config: InstallProfileConfig(sourceURL: source), fileManager: testFM ) - #expect(!first.overwritten) + XCTAssertFalse(first.overwritten) // Change the source contents. let newBytes: [UInt8] = (0..<256).map { UInt8(($0 + 100) % 256) } @@ -87,15 +84,14 @@ struct ProfileInstallerTests { fileManager: testFM ) - #expect(second.overwritten) - #expect(!second.renamed) - #expect(fm.fileExists(atPath: second.destPath)) + XCTAssertTrue(second.overwritten) + XCTAssertFalse(second.renamed) + XCTAssertTrue(fm.fileExists(atPath: second.destPath)) let installed = try Data(contentsOf: URL(fileURLWithPath: second.destPath)) - #expect(Array(installed) == newBytes) + XCTAssertEqual(Array(installed), newBytes) } - @Test("Preserves .icm source extension") - func preservesIcmExtension() throws { + func testPreservesIcmExtension() throws { let tmp = try makeTempDir() let testFM = TestFileManager(home: tmp) let source = try makeSource(at: tmp, name: "m5_profile.icm") @@ -105,12 +101,11 @@ struct ProfileInstallerTests { fileManager: testFM ) - #expect(URL(fileURLWithPath: result.destPath).pathExtension == "icm") - #expect(result.destPath.hasSuffix("m5_profile.icm")) + XCTAssertEqual(URL(fileURLWithPath: result.destPath).pathExtension, "icm") + XCTAssertTrue(result.destPath.hasSuffix("m5_profile.icm")) } - @Test("Rejects parent traversal in source path") - func rejectsParentTraversal() throws { + func testRejectsParentTraversal() throws { let fm = FileManager.default let tmp = try makeTempDir() @@ -125,20 +120,19 @@ struct ProfileInstallerTests { let sourceURL = tmp .appendingPathComponent("..") .appendingPathComponent(naughtyName) - #expect(fm.fileExists(atPath: sourceURL.path)) + XCTAssertTrue(fm.fileExists(atPath: sourceURL.path)) do { _ = try ProfileInstaller.install(config: InstallProfileConfig(sourceURL: sourceURL)) - Issue.record("Expected unsafeStem error") + XCTFail("Expected unsafeStem error") } catch let error as ProfileInstallError { - if case .unsafeStem = error { } else { Issue.record("Expected unsafeStem, got \(error)") } + if case .unsafeStem = error { } else { XCTFail("Expected unsafeStem, got \(error)") } } catch { - Issue.record("Unexpected error type: \(error)") + XCTFail("Unexpected error type: \(error)") } } - @Test("Allows stems with consecutive dots like foo..bar") - func allowsDoubleDotStem() throws { + func testAllowsDoubleDotStem() throws { let fm = FileManager.default let tmp = try makeTempDir() let testFM = TestFileManager(home: tmp) @@ -149,23 +143,22 @@ struct ProfileInstallerTests { fileManager: testFM ) - #expect(result.destPath.hasSuffix("foo..bar.icc")) - #expect(fm.fileExists(atPath: result.destPath)) + XCTAssertTrue(result.destPath.hasSuffix("foo..bar.icc")) + XCTAssertTrue(fm.fileExists(atPath: result.destPath)) } - @Test("Rejects source files that are too small") - func rejectsSmallSource() throws { + func testRejectsSmallSource() throws { let tmp = try makeTempDir() let source = tmp.appendingPathComponent("tiny.icc") try Data(repeating: 0, count: 64).write(to: source) do { _ = try ProfileInstaller.install(config: InstallProfileConfig(sourceURL: source)) - Issue.record("Expected sourceTooSmall error") + XCTFail("Expected sourceTooSmall error") } catch let error as ProfileInstallError { - if case .sourceTooSmall = error { } else { Issue.record("Expected sourceTooSmall, got \(error)") } + if case .sourceTooSmall = error { } else { XCTFail("Expected sourceTooSmall, got \(error)") } } catch { - Issue.record("Unexpected error type: \(error)") + XCTFail("Unexpected error type: \(error)") } } } diff --git a/Tests/ICCeryCoreTests/SettingsTests.swift b/Tests/ICCeryCoreTests/SettingsTests.swift index 9b86b15..c8ae705 100644 --- a/Tests/ICCeryCoreTests/SettingsTests.swift +++ b/Tests/ICCeryCoreTests/SettingsTests.swift @@ -1,5 +1,5 @@ -import Testing import Foundation +import XCTest @testable import ICCeryCore private func tempStoreURL() -> URL { @@ -8,92 +8,90 @@ private func tempStoreURL() -> URL { .appendingPathComponent("settings.json") } -@Suite("AppSettings") -struct AppSettingsTests { - @Test func defaults() { +final class AppSettingsTests: XCTestCase { + func testDefaults() { let s = AppSettings.default - #expect(s.argyllBinaryDir == nil) - #expect(s.defaultInstrument == nil) - #expect(s.logLevel == nil) - #expect(s.deltaEGoodMax == 2.0) - #expect(s.deltaEWarningMax == 5.0) - #expect(s.customPresets.isEmpty) - #expect(!s.enableI1Pro2Leds) - #expect(s.calibrationStaleDays == 30) - #expect(s.defaultInstallLocation == .user) - #expect(s.askBeforeOverwriteProfile) - #expect(!s.openColorPanelAfterInstall) - #expect(s.isValid) + XCTAssertNil(s.argyllBinaryDir) + XCTAssertNil(s.defaultInstrument) + XCTAssertNil(s.logLevel) + XCTAssertEqual(s.deltaEGoodMax, 2.0) + XCTAssertEqual(s.deltaEWarningMax, 5.0) + XCTAssertTrue(s.customPresets.isEmpty) + XCTAssertFalse(s.enableI1Pro2Leds) + XCTAssertEqual(s.calibrationStaleDays, 30) + XCTAssertEqual(s.defaultInstallLocation, .user) + XCTAssertTrue(s.askBeforeOverwriteProfile) + XCTAssertFalse(s.openColorPanelAfterInstall) + XCTAssertTrue(s.isValid) } - @Test func negativeThresholds() { + func testNegativeThresholds() { var s = AppSettings.default s.deltaEGoodMax = -1 - #expect(s.validate() == [AppSettings.errorNegativeDeltaE]) + XCTAssertEqual(s.validate(), [AppSettings.errorNegativeDeltaE]) s.deltaEGoodMax = 2.0 s.deltaEWarningMax = -0.5 // -0.5 < 0 → negative error; good(2.0) >= warn(-0.5) → order error too - #expect(s.validate() == [ + XCTAssertTrue(s.validate() == [ AppSettings.errorNegativeDeltaE, AppSettings.errorThresholdOrder, ]) } - @Test func goodMustBeStrictlyLessThanWarning() { + func testGoodMustBeStrictlyLessThanWarning() { var s = AppSettings.default s.deltaEGoodMax = 5.0 - #expect(s.validate() == [AppSettings.errorThresholdOrder]) + XCTAssertEqual(s.validate(), [AppSettings.errorThresholdOrder]) s.deltaEGoodMax = 6.0 - #expect(s.validate() == [AppSettings.errorThresholdOrder]) + XCTAssertEqual(s.validate(), [AppSettings.errorThresholdOrder]) s.deltaEGoodMax = 4.9 - #expect(s.isValid) + XCTAssertTrue(s.isValid) } - @Test func snakeCaseKeys() throws { + func testSnakeCaseKeys() throws { let s = AppSettings.default let data = try JSONEncoder().encode(s) let json = String(data: data, encoding: .utf8)! - #expect(json.contains("\"delta_e_good_max\"")) - #expect(json.contains("\"default_install_location\"")) - #expect(json.contains("\"enable_i1pro2_leds\"")) + XCTAssertTrue(json.contains("\"delta_e_good_max\"")) + XCTAssertTrue(json.contains("\"default_install_location\"")) + XCTAssertTrue(json.contains("\"enable_i1pro2_leds\"")) } } -@Suite("SettingsStore") -struct SettingsStoreTests { - @Test func roundTrip() throws { +final class SettingsStoreTests: XCTestCase { + func testRoundTrip() throws { let url = tempStoreURL() let store = SettingsStore(fileURL: url) var s = AppSettings.default s.deltaEGoodMax = 1.5 s.defaultInstrument = "p3" try store.save(s) - #expect(store.load() == s) + XCTAssertEqual(store.load(), s) } - @Test func corruptJsonFallsBackToDefaults() throws { + func testCorruptJsonFallsBackToDefaults() throws { let url = tempStoreURL() try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true ) try "{ not json".write(to: url, atomically: true, encoding: .utf8) - #expect(SettingsStore(fileURL: url).load() == .default) + XCTAssertEqual(SettingsStore(fileURL: url).load(), .default) } - @Test func missingFileReturnsDefaults() { - #expect(SettingsStore(fileURL: tempStoreURL()).load() == .default) + func testMissingFileReturnsDefaults() { + XCTAssertEqual(SettingsStore(fileURL: tempStoreURL()).load(), .default) } - @Test func invalidSettingsNotPersisted() throws { + func testInvalidSettingsNotPersisted() throws { let url = tempStoreURL() let store = SettingsStore(fileURL: url) var s = AppSettings.default s.deltaEGoodMax = 9.0 // >= warning 5.0 - #expect(throws: SettingsStore.SettingsError.self) { try store.save(s) } - #expect(!FileManager.default.fileExists(atPath: url.path)) + XCTAssertThrowsError(try store.save(s)) { error in XCTAssertTrue(error is SettingsStore.SettingsError) } + XCTAssertFalse(FileManager.default.fileExists(atPath: url.path)) } - @Test func invalidSaveOverValidFilePreservesBytesAndPostsNothing() throws { + func testInvalidSaveOverValidFilePreservesBytesAndPostsNothing() throws { let url = tempStoreURL() let store = SettingsStore(fileURL: url) var valid = AppSettings.default @@ -109,13 +107,13 @@ struct SettingsStoreTests { var invalid = AppSettings.default invalid.deltaEGoodMax = 9.0 - #expect(throws: SettingsStore.SettingsError.self) { try store.save(invalid) } - #expect(try Data(contentsOf: url) == originalBytes) - #expect(!fired) - #expect(store.load() == valid) + XCTAssertThrowsError(try store.save(invalid)) { error in XCTAssertTrue(error is SettingsStore.SettingsError) } + XCTAssertEqual(try Data(contentsOf: url), originalBytes) + XCTAssertFalse(fired) + XCTAssertEqual(store.load(), valid) } - @Test func savePostsNotification() async throws { + func testSavePostsNotification() async throws { let url = tempStoreURL() let store = SettingsStore(fileURL: url) var fired = false @@ -124,12 +122,11 @@ struct SettingsStoreTests { ) { _ in fired = true } defer { NotificationCenter.default.removeObserver(token) } try store.save(.default) - #expect(fired) + XCTAssertTrue(fired) } } -@Suite("LogSink") -struct LogSinkTests { +final class LogSinkTests: XCTestCase { private func tempLog() -> (URL, LogSink) { let url = FileManager.default.temporaryDirectory .appendingPathComponent("iccery-log-\(UUID().uuidString)") @@ -137,26 +134,26 @@ struct LogSinkTests { return (url, LogSink(fileURL: url)) } - @Test func writesFormattedLines() { + func testWritesFormattedLines() { let (url, sink) = tempLog() sink.setLevel(.debug) sink.write(level: .info, category: "test", message: "hello") let content = (try? String(contentsOf: url, encoding: .utf8)) ?? "" - #expect(content.contains("[INFO] test: hello")) + XCTAssertTrue(content.contains("[INFO] test: hello")) } - @Test func levelFilteringIsLive() { + func testLevelFilteringIsLive() { let (url, sink) = tempLog() sink.setLevel(.error) sink.write(level: .info, category: "t", message: "hidden") sink.setLevel(.info) // runtime change, no restart (#158) sink.write(level: .info, category: "t", message: "shown") let content = (try? String(contentsOf: url, encoding: .utf8)) ?? "" - #expect(!content.contains("hidden")) - #expect(content.contains("shown")) + XCTAssertFalse(content.contains("hidden")) + XCTAssertTrue(content.contains("shown")) } - @Test func rotatesAt5MiBKeeping5Segments() throws { + func testRotatesAt5MiBKeeping5Segments() throws { let (url, sink) = tempLog() sink.setLevel(.trace) // Pre-fill the active log just under the cap, then cross it. @@ -167,20 +164,20 @@ struct LogSinkTests { try big.write(to: url, atomically: true, encoding: .utf8) sink.write(level: .info, category: "t", message: "trigger rotation") - #expect(FileManager.default.fileExists( + XCTAssertTrue(FileManager.default.fileExists( atPath: url.appendingPathExtension("1").path )) // Active log is small again. let size = try FileManager.default.attributesOfItem( atPath: url.path )[.size] as? UInt64 - #expect((size ?? 0) < 1024) + XCTAssertTrue((size ?? 0) < 1024) } - @Test func tailExcerptCaps() throws { + func testTailExcerptCaps() throws { let (url, sink) = tempLog() sink.setLevel(.debug) sink.write(level: .info, category: "t", message: "line") - #expect(sink.tailExcerpt(maxBytes: 8).count <= 8) + XCTAssertTrue(sink.tailExcerpt(maxBytes: 8).count <= 8) } } diff --git a/Tests/ICCeryCoreTests/TargenTests.swift b/Tests/ICCeryCoreTests/TargenTests.swift index c11a0a1..ce2f2d8 100644 --- a/Tests/ICCeryCoreTests/TargenTests.swift +++ b/Tests/ICCeryCoreTests/TargenTests.swift @@ -1,4 +1,3 @@ -import Testing import XCTest import Foundation @testable import ICCeryCore @@ -220,11 +219,9 @@ final class TargenArgsTests: XCTestCase { } } -@Suite("ArgyllRunner Targen") -struct ArgyllRunnerTargenTests { +final class ArgyllRunnerTargenTests: XCTestCase { - @Test("Successful targen execution creates .ti1 and returns URL") - func successfulTargenExecution() async throws { + func testSuccessfulTargenExecution() async throws { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } @@ -270,14 +267,13 @@ struct ArgyllRunnerTargenTests { box.append(batch) } logLines = box.lines - #expect(logLines.contains("Generating patches...")) + XCTAssertTrue(logLines.contains("Generating patches...")) - #expect(FileManager.default.fileExists(atPath: ti1URL.path)) - #expect(ti1URL.lastPathComponent == "mock_test.ti1") + XCTAssertTrue(FileManager.default.fileExists(atPath: ti1URL.path)) + XCTAssertEqual(ti1URL.lastPathComponent, "mock_test.ti1") } - @Test("Failed targen execution throws toolFailed") - func failedTargenExecution() async throws { + func testFailedTargenExecution() async throws { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } @@ -304,14 +300,15 @@ struct ArgyllRunnerTargenTests { workingDirectory: tempDir ) - await #expect(throws: ArgyllRunnerError.toolFailed( - tool: "targen", code: 1, logs: ["Error: something went wrong"])) { + await assertAsyncThrows(expectedType: ArgyllRunnerError.self) { try await runner.runTargen(config: config) + } errorHandler: { error in + XCTAssertEqual(error, .toolFailed( + tool: "targen", code: 1, logs: ["Error: something went wrong"])) } } - @Test("Targen exit 0 without .ti1 throws missingArtefact") - func missingArtefactThrows() async throws { + func testMissingArtefactThrows() async throws { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } @@ -338,9 +335,11 @@ struct ArgyllRunnerTargenTests { workingDirectory: tempDir ) - await #expect(throws: ArgyllRunnerError.missingArtefact( - tempDir.appendingPathComponent("no_file.ti1").path)) { + await assertAsyncThrows(expectedType: ArgyllRunnerError.self) { try await runner.runTargen(config: config) + } errorHandler: { error in + XCTAssertEqual(error, .missingArtefact( + tempDir.appendingPathComponent("no_file.ti1").path)) } } } diff --git a/Tests/ICCeryCoreTests/TargetWorkflowViewModelTests.swift b/Tests/ICCeryCoreTests/TargetWorkflowViewModelTests.swift index beac3f3..84e13e9 100644 --- a/Tests/ICCeryCoreTests/TargetWorkflowViewModelTests.swift +++ b/Tests/ICCeryCoreTests/TargetWorkflowViewModelTests.swift @@ -1,17 +1,15 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore @testable import ICCery /// Dataset-import error contracts through the /// `importMeasurementDataset(from:)` seam (issue #80): parser and I/O /// failures must surface identically as a single `.error` Notice. -@Suite("TargetWorkflowViewModel dataset import") @MainActor -struct TargetWorkflowViewModelTests { +final class TargetWorkflowViewModelTests: XCTestCase { - @Test("Malformed content (CGATSParseError) produces one .error notice prefixed 'Import failed:'") - func malformedDatasetNotice() throws { + func testMalformedDatasetNotice() throws { let env = try TestAppEnvironment.make() defer { env.cleanup() } let vm = TargetWorkflowViewModel(environment: env.environment) @@ -21,13 +19,12 @@ struct TargetWorkflowViewModelTests { vm.importMeasurementDataset(from: bad) - let notice = try #require(vm.wizard.notice) - #expect(notice.kind == .error) - #expect(notice.text.hasPrefix("Import failed:")) + let notice = try XCTUnwrap(vm.wizard.notice) + XCTAssertEqual(notice.kind, .error) + XCTAssertTrue(notice.text.hasPrefix("Import failed:")) } - @Test("Missing file (CocoaError) produces one .error notice prefixed 'Import failed:'") - func missingDatasetNotice() throws { + func testMissingDatasetNotice() throws { let env = try TestAppEnvironment.make() defer { env.cleanup() } let vm = TargetWorkflowViewModel(environment: env.environment) @@ -35,8 +32,8 @@ struct TargetWorkflowViewModelTests { let missing = env.root.appendingPathComponent("does-not-exist.ti3") vm.importMeasurementDataset(from: missing) - let notice = try #require(vm.wizard.notice) - #expect(notice.kind == .error) - #expect(notice.text.hasPrefix("Import failed:")) + let notice = try XCTUnwrap(vm.wizard.notice) + XCTAssertEqual(notice.kind, .error) + XCTAssertTrue(notice.text.hasPrefix("Import failed:")) } } diff --git a/Tests/ICCeryCoreTests/VerificationHistoryStoreTests.swift b/Tests/ICCeryCoreTests/VerificationHistoryStoreTests.swift index 313b297..7f2ae46 100644 --- a/Tests/ICCeryCoreTests/VerificationHistoryStoreTests.swift +++ b/Tests/ICCeryCoreTests/VerificationHistoryStoreTests.swift @@ -1,12 +1,10 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("VerificationHistoryStore") -struct VerificationHistoryStoreTests { +final class VerificationHistoryStoreTests: XCTestCase { - @Test("Append and cap") - func appendAndCap() async throws { + func testAppendAndCap() async throws { let fm = FileManager.default let tmp = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString) try fm.createDirectory(at: tmp, withIntermediateDirectories: true) @@ -29,12 +27,11 @@ struct VerificationHistoryStoreTests { } let all = await store.all() - #expect(all.count == 3) - #expect(all.first?.avgDE == 2.0) + XCTAssertEqual(all.count, 3) + XCTAssertEqual(all.first?.avgDE, 2.0) } - @Test("Parse failure preserves file") - func parseFailurePreservesFile() async { + func testParseFailurePreservesFile() async { let fm = FileManager.default let tmp = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString) try? fm.createDirectory(at: tmp, withIntermediateDirectories: true) @@ -45,14 +42,13 @@ struct VerificationHistoryStoreTests { let store = VerificationHistoryStore(url: url) do { _ = try await store.load() - Issue.record("load() should throw on invalid JSON") + XCTFail("load() should throw on invalid JSON") } catch { - #expect(fm.fileExists(atPath: url.path)) + XCTAssertTrue(fm.fileExists(atPath: url.path)) } } - @Test("Append loads existing records first") - func appendLoadsExisting() async throws { + func testAppendLoadsExisting() async throws { let fm = FileManager.default let tmp = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString) try fm.createDirectory(at: tmp, withIntermediateDirectories: true) @@ -89,13 +85,12 @@ struct VerificationHistoryStoreTests { _ = try await store2.append(new) let all = await store2.all() - #expect(all.count == 2) - #expect(all.contains { $0.id == "vr-existing" }) - #expect(all.contains { $0.id == "vr-new" }) + XCTAssertEqual(all.count, 2) + XCTAssertTrue(all.contains { $0.id == "vr-existing" }) + XCTAssertTrue(all.contains { $0.id == "vr-new" }) } - @Test("Append does not overwrite an unparseable file") - func appendPreservesUnparseableFile() async { + func testAppendPreservesUnparseableFile() async { let fm = FileManager.default let tmp = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString) try? fm.createDirectory(at: tmp, withIntermediateDirectories: true) @@ -119,20 +114,19 @@ struct VerificationHistoryStoreTests { do { _ = try await store.append(record) - Issue.record("append() should propagate the load error") + XCTFail("append() should propagate the load error") } catch { - #expect(fm.fileExists(atPath: url.path)) + XCTAssertTrue(fm.fileExists(atPath: url.path)) if let data = try? Data(contentsOf: url), let contents = String(data: data, encoding: .utf8) { - #expect(contents == badJSON) + XCTAssertEqual(contents, badJSON) } else { - Issue.record("Could not read preserved file") + XCTFail("Could not read preserved file") } } } - @Test("Clear does not overwrite an unparseable file") - func clearPreservesUnparseableFile() async { + func testClearPreservesUnparseableFile() async { let fm = FileManager.default let tmp = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString) try? fm.createDirectory(at: tmp, withIntermediateDirectories: true) @@ -144,15 +138,14 @@ struct VerificationHistoryStoreTests { let store = VerificationHistoryStore(url: url) do { try await store.clear() - Issue.record("clear() should propagate the load error") + XCTFail("clear() should propagate the load error") } catch { let contents = try? String(contentsOf: url, encoding: .utf8) - #expect(contents == badJSON) + XCTAssertEqual(contents, badJSON) } } - @Test("ISO-8601 timestamps round-trip through a fresh store") - func iso8601RoundTrip() async throws { + func testIso8601RoundTrip() async throws { let fm = FileManager.default let tmp = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString) try fm.createDirectory(at: tmp, withIntermediateDirectories: true) @@ -174,16 +167,15 @@ struct VerificationHistoryStoreTests { _ = try await store1.append(record) let text = try String(contentsOf: url, encoding: .utf8) - #expect(text.contains(ISO8601DateFormatter().string(from: timestamp))) + XCTAssertTrue(text.contains(ISO8601DateFormatter().string(from: timestamp))) let store2 = VerificationHistoryStore(url: url) let loaded = try await store2.load() - #expect(loaded.count == 1) - #expect(loaded.first?.timestamp == timestamp) + XCTAssertEqual(loaded.count, 1) + XCTAssertEqual(loaded.first?.timestamp, timestamp) } - @Test("CSV export quoting") - func csvQuoting() async throws { + func testCsvQuoting() async throws { let fm = FileManager.default let tmp = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString) try fm.createDirectory(at: tmp, withIntermediateDirectories: true) @@ -204,7 +196,7 @@ struct VerificationHistoryStoreTests { _ = try await store.append(record) let csv = await store.exportCSV() - #expect(csv.contains("\"a,b\"")) - #expect(csv.contains("\"\"quoted\"\"")) + XCTAssertTrue(csv.contains("\"a,b\"")) + XCTAssertTrue(csv.contains("\"\"quoted\"\"")) } } diff --git a/Tests/ICCeryCoreTests/WizardCalibrationSessionTests.swift b/Tests/ICCeryCoreTests/WizardCalibrationSessionTests.swift index 42be31c..392fe4c 100644 --- a/Tests/ICCeryCoreTests/WizardCalibrationSessionTests.swift +++ b/Tests/ICCeryCoreTests/WizardCalibrationSessionTests.swift @@ -1,13 +1,12 @@ -import Testing import Foundation +import XCTest @testable import ICCeryCore @testable import ICCery /// Issue #29 — `CAL_` basename must be restored on relaunch and on any /// attempt to navigate to a non-calibration stage that would use it. -@Suite("WizardCalibrationSession") @MainActor -struct WizardCalibrationSessionTests { +final class WizardCalibrationSessionTests: XCTestCase { private func tempURL() -> URL { FileManager.default.temporaryDirectory @@ -24,8 +23,7 @@ struct WizardCalibrationSessionTests { return url } - @Test("Persist and restore calibrationOriginalBasename across a relaunch") - func relaunchRestoresOriginal() throws { + func testRelaunchRestoresOriginal() throws { let url = tempURL() let store = WizardStateStore(fileURL: url) var saved = WizardState( @@ -39,14 +37,13 @@ struct WizardCalibrationSessionTests { let model = WizardViewModel(stateStore: store) - #expect(model.basename == "DemoTarget") - #expect(model.calibrationOriginalBasename == "") - #expect(model.sessionMode == .profile) - #expect(model.stage == .generate) + XCTAssertEqual(model.basename, "DemoTarget") + XCTAssertEqual(model.calibrationOriginalBasename, "") + XCTAssertEqual(model.sessionMode, .profile) + XCTAssertEqual(model.stage, .generate) } - @Test("go(to: .buildProfile) while basename is CAL_ refuses and restores the original") - func goToBuildProfileRefusesAndRestores() throws { + func testGoToBuildProfileRefusesAndRestores() throws { let dir = try tempDir() let url = tempURL() let store = WizardStateStore(fileURL: url) @@ -60,14 +57,13 @@ struct WizardCalibrationSessionTests { model.go(to: .buildProfile) - #expect(model.basename == "DemoTarget") - #expect(model.calibrationOriginalBasename == "") - #expect(model.sessionMode == .profile) - #expect(model.stage == .calibrate) + XCTAssertEqual(model.basename, "DemoTarget") + XCTAssertEqual(model.calibrationOriginalBasename, "") + XCTAssertEqual(model.sessionMode, .profile) + XCTAssertEqual(model.stage, .calibrate) } - @Test("go(to: .layOutPrint) while basename is CAL_ stays in calibration") - func goToLayoutStaysCal() throws { + func testGoToLayoutStaysCal() throws { let dir = try tempDir() let url = tempURL() let store = WizardStateStore(fileURL: url) @@ -81,9 +77,9 @@ struct WizardCalibrationSessionTests { model.go(to: .layOutPrint) - #expect(model.basename == "CAL_DemoTarget") - #expect(model.calibrationOriginalBasename == "DemoTarget") - #expect(model.sessionMode == .calibration) - #expect(model.stage == .layOutPrint) + XCTAssertEqual(model.basename, "CAL_DemoTarget") + XCTAssertEqual(model.calibrationOriginalBasename, "DemoTarget") + XCTAssertEqual(model.sessionMode, .calibration) + XCTAssertEqual(model.stage, .layOutPrint) } } diff --git a/Tests/ICCeryCoreTests/WizardGatingTests.swift b/Tests/ICCeryCoreTests/WizardGatingTests.swift index 2f8a6b9..3fbde91 100644 --- a/Tests/ICCeryCoreTests/WizardGatingTests.swift +++ b/Tests/ICCeryCoreTests/WizardGatingTests.swift @@ -1,5 +1,5 @@ -import Testing import Foundation +import XCTest @testable import ICCeryCore private func artefacts( @@ -16,77 +16,75 @@ private func artefacts( return a } -@Suite("WizardGating matrix") -struct WizardGatingTests { +final class WizardGatingTests: XCTestCase { - @Test func emptyProjectOnlyStage1() { + func testEmptyProjectOnlyStage1() { let a = artefacts() - #expect(WizardGating.isUnlocked(.generate, artefacts: a)) - #expect(WizardGating.isUnlocked(.calibrate, artefacts: a)) + XCTAssertTrue(WizardGating.isUnlocked(.generate, artefacts: a)) + XCTAssertTrue(WizardGating.isUnlocked(.calibrate, artefacts: a)) for s in [WizardStage.layOutPrint, .measure, .buildProfile, .verifyInstall] { - #expect(!WizardGating.isUnlocked(s, artefacts: a), "\(s) should be locked") + XCTAssertFalse(WizardGating.isUnlocked(s, artefacts: a), "\(s) should be locked") } } - @Test func ti1UnlocksStage2Only() { + func testTi1UnlocksStage2Only() { let a = artefacts(ti1: true) - #expect(WizardGating.isUnlocked(.layOutPrint, artefacts: a)) - #expect(!WizardGating.isUnlocked(.measure, artefacts: a)) - #expect(!WizardGating.isUnlocked(.buildProfile, artefacts: a)) - #expect(!WizardGating.isUnlocked(.verifyInstall, artefacts: a)) + XCTAssertTrue(WizardGating.isUnlocked(.layOutPrint, artefacts: a)) + XCTAssertFalse(WizardGating.isUnlocked(.measure, artefacts: a)) + XCTAssertFalse(WizardGating.isUnlocked(.buildProfile, artefacts: a)) + XCTAssertFalse(WizardGating.isUnlocked(.verifyInstall, artefacts: a)) } - @Test func stage3NeedsTi1AndTi2() { - #expect(!WizardGating.isUnlocked(.measure, artefacts: artefacts(ti2: true))) - #expect(WizardGating.isUnlocked(.measure, artefacts: artefacts(ti1: true, ti2: true))) + func testStage3NeedsTi1AndTi2() { + XCTAssertFalse(WizardGating.isUnlocked(.measure, artefacts: artefacts(ti2: true))) + XCTAssertTrue(WizardGating.isUnlocked(.measure, artefacts: artefacts(ti1: true, ti2: true))) } - @Test func stage4NeedsTi3NotTi2() { + func testStage4NeedsTi3NotTi2() { // #109/#110: .ti2 alone must never unlock Stage 4. let a = artefacts(ti1: true, ti2: true) - #expect(!WizardGating.isUnlocked(.buildProfile, artefacts: a)) - #expect(WizardGating.isUnlocked(.buildProfile, artefacts: artefacts(ti3: true))) + XCTAssertFalse(WizardGating.isUnlocked(.buildProfile, artefacts: a)) + XCTAssertTrue(WizardGating.isUnlocked(.buildProfile, artefacts: artefacts(ti3: true))) } - @Test func stage5NeedsTi3AndProfile() { - #expect(!WizardGating.isUnlocked(.verifyInstall, artefacts: artefacts(ti3: true))) - #expect(!WizardGating.isUnlocked(.verifyInstall, artefacts: artefacts(profile: true))) - #expect(WizardGating.isUnlocked( + func testStage5NeedsTi3AndProfile() { + XCTAssertFalse(WizardGating.isUnlocked(.verifyInstall, artefacts: artefacts(ti3: true))) + XCTAssertFalse(WizardGating.isUnlocked(.verifyInstall, artefacts: artefacts(profile: true))) + XCTAssertTrue(WizardGating.isUnlocked( .verifyInstall, artefacts: artefacts(ti3: true, profile: true) )) } - @Test func forwardGatedBackwardFree() { + func testForwardGatedBackwardFree() { let a = artefacts() - #expect(!WizardGating.canNavigate(to: .layOutPrint, from: .generate, artefacts: a)) + XCTAssertFalse(WizardGating.canNavigate(to: .layOutPrint, from: .generate, artefacts: a)) // Backward always allowed even when artefacts vanished. - #expect(WizardGating.canNavigate(to: .generate, from: .measure, artefacts: a)) + XCTAssertTrue(WizardGating.canNavigate(to: .generate, from: .measure, artefacts: a)) // Same stage is a no-op. - #expect(WizardGating.canNavigate(to: .measure, from: .measure, artefacts: a)) + XCTAssertTrue(WizardGating.canNavigate(to: .measure, from: .measure, artefacts: a)) // Stage 0 is a side-trip, never gated. - #expect(WizardGating.canNavigate(to: .calibrate, from: .generate, artefacts: a)) + XCTAssertTrue(WizardGating.canNavigate(to: .calibrate, from: .generate, artefacts: a)) } - @Test func deepestUnlocked() { - #expect(WizardGating.deepestUnlocked(artefacts: artefacts()) == .generate) - #expect(WizardGating.deepestUnlocked( + func testDeepestUnlocked() { + XCTAssertEqual(WizardGating.deepestUnlocked(artefacts: artefacts()), .generate) + XCTAssertEqual(WizardGating.deepestUnlocked( artefacts: artefacts(ti1: true, ti2: true) - ) == .measure) - #expect(WizardGating.deepestUnlocked( + ), .measure) + XCTAssertEqual(WizardGating.deepestUnlocked( artefacts: artefacts(ti3: true, profile: true) - ) == .verifyInstall) + ), .verifyInstall) } } -@Suite("WizardStateStore") -struct WizardStateStoreTests { +final class WizardStateStoreTests: XCTestCase { private func tempURL() -> URL { FileManager.default.temporaryDirectory .appendingPathComponent("iccery-wiz-\(UUID().uuidString)") .appendingPathComponent("wizard_state.json") } - @Test func roundTrip() throws { + func testRoundTrip() throws { let url = tempURL() let store = WizardStateStore(fileURL: url) var s = WizardState() @@ -97,43 +95,43 @@ struct WizardStateStoreTests { s.profileBasename = "imported" s.calibrationOriginalBasename = "pre-cal" try store.save(s) - #expect(store.load() == s) + XCTAssertEqual(store.load(), s) } - @Test func missingFileDefaults() { + func testMissingFileDefaults() { let s = WizardStateStore(fileURL: tempURL()).load() - #expect(s == .default) - #expect(s.stage == .generate) - #expect(s.sessionMode == .profile) + XCTAssertEqual(s, .default) + XCTAssertEqual(s.stage, .generate) + XCTAssertEqual(s.sessionMode, .profile) } - @Test func corruptStageFallsBackToGenerate() throws { + func testCorruptStageFallsBackToGenerate() throws { let url = tempURL() try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true ) try #"{"current_stage": 99, "basename": "", "cwd": "", "session_mode": "profile"}"# .write(to: url, atomically: true, encoding: .utf8) - #expect(WizardStateStore(fileURL: url).load().stage == .generate) + XCTAssertEqual(WizardStateStore(fileURL: url).load().stage, .generate) } - @Test func corruptJsonReturnsDefaultAndKeepsBytes() throws { + func testCorruptJsonReturnsDefaultAndKeepsBytes() throws { let url = tempURL() try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true ) try "not json".write(to: url, atomically: true, encoding: .utf8) - #expect(WizardStateStore(fileURL: url).load() == .default) + XCTAssertEqual(WizardStateStore(fileURL: url).load(), .default) let kept = try String(contentsOf: url, encoding: .utf8) - #expect(kept == "not json") + XCTAssertEqual(kept, "not json") } - @Test func sessionModeCalibrationRoundTrips() throws { + func testSessionModeCalibrationRoundTrips() throws { var s = WizardState(sessionMode: .calibration) let data = try JSONEncoder().encode(s) let decoded = try JSONDecoder().decode(WizardState.self, from: data) - #expect(decoded.sessionMode == .calibration) + XCTAssertEqual(decoded.sessionMode, .calibration) s.sessionMode = .profile - #expect(s.sessionMode == .profile) + XCTAssertEqual(s.sessionMode, .profile) } } -- 2.39.5