diff --git a/Packages/ICCeryCore/Sources/ICCeryCore/CGATS/CGATSParser.swift b/Packages/ICCeryCore/Sources/ICCeryCore/CGATS/CGATSParser.swift index 327603a..8b0fc25 100644 --- a/Packages/ICCeryCore/Sources/ICCeryCore/CGATS/CGATSParser.swift +++ b/Packages/ICCeryCore/Sources/ICCeryCore/CGATS/CGATSParser.swift @@ -73,7 +73,7 @@ public enum CGATSParser { _ contents: String, sourceURL: URL? = nil ) throws -> CGATSDataset { - guard !contents.isEmpty else { throw .emptyFile } + guard !contents.isEmpty else { throw CGATSParseError.emptyFile } let ext = sourceURL?.pathExtension.lowercased() ?? "" let isCSV = ext == "csv" || contents.trimmingCharacters(in: .whitespacesAndNewlines) @@ -101,10 +101,10 @@ public enum CGATSParser { } guard let formatStart, let formatEnd, formatEnd > formatStart + 1 else { - throw .missingBeginDataFormat + throw CGATSParseError.missingBeginDataFormat } guard let dataStart, let dataEnd, dataEnd > dataStart + 1 else { - throw .missingBeginData + throw CGATSParseError.missingBeginData } let rawFieldNames = splitFields(lines[formatStart + 1]) @@ -139,7 +139,7 @@ public enum CGATSParser { let lineIndex = dataStart + offset let rawRow = splitFields(lines[lineIndex]) guard rawRow.count == fieldNames.count else { - throw .incorrectArity(line: lineIndex + 1, expected: fieldNames.count, got: rawRow.count) + throw CGATSParseError.incorrectArity(line: lineIndex + 1, expected: fieldNames.count, got: rawRow.count) } var sample = RawSample(id: String(offset), lineIndex: lineIndex) @@ -153,7 +153,7 @@ public enum CGATSParser { groupMax[group, default: 0] = max(groupMax[group, default: 0], number) } } else if !cleaned.isEmpty { - throw .nonNumericValue(field: name, value: raw, line: lineIndex + 1) + throw CGATSParseError.nonNumericValue(field: name, value: raw, line: lineIndex + 1) } } else { sample.strings[name] = raw @@ -239,7 +239,7 @@ public enum CGATSParser { lines.append(line) } - guard !lines.isEmpty else { throw .emptyFile } + guard !lines.isEmpty else { throw CGATSParseError.emptyFile } // Wrap a bare CSV / ISO28178 file in the canonical CGATS block // structure so the boundary-based parser below can handle it. diff --git a/Tests/ICCeryCoreTests/AppPathsTests.swift b/Tests/ICCeryCoreTests/AppPathsTests.swift index 2316056..130e0d8 100644 --- a/Tests/ICCeryCoreTests/AppPathsTests.swift +++ b/Tests/ICCeryCoreTests/AppPathsTests.swift @@ -1,27 +1,25 @@ -import Testing +import XCTest import Foundation @testable import ICCeryCore -@Suite("AppPaths") -struct AppPathsTests { - @Test func appDataDirUsesBundleID() { - #expect(AppPaths.appDataDir.path.contains("Library/Application Support/com.gronod.iccery2")) +final class AppPathsTests: XCTestCase { + func testAppDataDirUsesBundleID() { + XCTAssertTrue(AppPaths.appDataDir.path.contains("Library/Application Support/com.gronod.iccery2")) } - @Test func logFileIsUnderLibraryLogs() { - #expect(AppPaths.logFile.lastPathComponent == "iccery.log") - #expect(AppPaths.logFile.path.contains("Library/Logs/com.gronod.iccery2")) + func testLogFileIsUnderLibraryLogs() { + XCTAssertEqual(AppPaths.logFile.lastPathComponent, "iccery.log") + XCTAssertTrue(AppPaths.logFile.path.contains("Library/Logs/com.gronod.iccery2")) } - @Test func bundledArgyllDirIsInsideResources() { - #expect(AppPaths.bundledArgyllDir.lastPathComponent == "Argyll") + func testBundledArgyllDirIsInsideResources() { + XCTAssertEqual(AppPaths.bundledArgyllDir.lastPathComponent, "Argyll") } } -@Suite("WizardStage") -struct WizardStageTests { - @Test func stepperOrderIsOneThroughFive() { - #expect(WizardStage.stepperStages.map(\.stepperIndex) == [1, 2, 3, 4, 5]) - #expect(WizardStage.calibrate.stepperIndex == nil) +final class WizardStageTests: XCTestCase { + func testStepperOrderIsOneThroughFive() { + XCTAssertEqual(WizardStage.stepperStages.map(\.stepperIndex), [1, 2, 3, 4, 5]) + XCTAssertNil(WizardStage.calibrate.stepperIndex) } } diff --git a/Tests/ICCeryCoreTests/ApplycalArgsTests.swift b/Tests/ICCeryCoreTests/ApplycalArgsTests.swift index fb10001..b4cb09f 100644 --- a/Tests/ICCeryCoreTests/ApplycalArgsTests.swift +++ b/Tests/ICCeryCoreTests/ApplycalArgsTests.swift @@ -1,22 +1,19 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("ApplycalArgs") -struct ApplycalArgsTests { +final class ApplycalArgsTests: XCTestCase { - @Test("Apply argv") - func applyArgv() throws { + func testApplyArgv() throws { let config = ApplycalConfig( calibrationPath: "/tmp/cal.cal", inputProfileURL: URL(fileURLWithPath: "/tmp/profile.icc") ) let args = try ApplycalArgs.build(config: config) - #expect(args == ["-v", "-a", "/tmp/cal.cal", "/tmp/profile.icc"]) + XCTAssertEqual(args, ["-v", "-a", "/tmp/cal.cal", "/tmp/profile.icc"]) } - @Test("Unapply is emitted when the caller explicitly sets it") - func unapplyEmittedWhenConfigSet() throws { + func testUnapplyEmittedWhenConfigSet() throws { let config = ApplycalConfig( calibrationPath: "/tmp/cal.cal", inputProfileURL: URL(fileURLWithPath: "/tmp/profile.icc"), @@ -25,6 +22,6 @@ struct ApplycalArgsTests { let args = try ApplycalArgs.build(config: config) // Builder emits -u only when the caller explicitly sets unapply. // The UI layer never passes unapply: true in v2.0. - #expect(args == ["-v", "-u", "/tmp/cal.cal", "/tmp/profile.icc"]) + XCTAssertEqual(args, ["-v", "-u", "/tmp/cal.cal", "/tmp/profile.icc"]) } } diff --git a/Tests/ICCeryCoreTests/ArgsBuilderTests.swift b/Tests/ICCeryCoreTests/ArgsBuilderTests.swift index 51e9605..05596c3 100644 --- a/Tests/ICCeryCoreTests/ArgsBuilderTests.swift +++ b/Tests/ICCeryCoreTests/ArgsBuilderTests.swift @@ -1,94 +1,80 @@ -import Testing +import XCTest import Foundation @testable import ICCeryCore -@Suite("ArgsBuilder") -struct ArgsBuilderTests { +final class ArgsBuilderTests: XCTestCase { // MARK: - option - @Test("option: nil emits nothing") - func optionNil() { - #expect(ArgsBuilder.option("-f", nil) == []) + func testOptionNil() { + XCTAssertEqual(ArgsBuilder.option("-f", nil), []) } - @Test("option: present value emits flag and value verbatim") - func optionPresent() { - #expect(ArgsBuilder.option("-f", "abc") == ["-f", "abc"]) - #expect(ArgsBuilder.option("-f", "") == ["-f", ""]) - #expect(ArgsBuilder.option("-f", " padded ") == ["-f", " padded "]) + func testOptionPresent() { + XCTAssertEqual(ArgsBuilder.option("-f", "abc"), ["-f", "abc"]) + XCTAssertEqual(ArgsBuilder.option("-f", ""), ["-f", ""]) + XCTAssertEqual(ArgsBuilder.option("-f", " padded "), ["-f", " padded "]) } // MARK: - optionIfNonEmpty - @Test("optionIfNonEmpty: nil and empty emit nothing") - func optionIfNonEmptyNilEmpty() { - #expect(ArgsBuilder.optionIfNonEmpty("-d", nil) == []) - #expect(ArgsBuilder.optionIfNonEmpty("-d", "") == []) + func testOptionIfNonEmptyNilEmpty() { + XCTAssertEqual(ArgsBuilder.optionIfNonEmpty("-d", nil), []) + XCTAssertEqual(ArgsBuilder.optionIfNonEmpty("-d", ""), []) } - @Test("optionIfNonEmpty: whitespace-only emits nothing") - func optionIfNonEmptyWhitespace() { - #expect(ArgsBuilder.optionIfNonEmpty("-d", " ") == []) - #expect(ArgsBuilder.optionIfNonEmpty("-d", " \t\n ") == []) + func testOptionIfNonEmptyWhitespace() { + XCTAssertEqual(ArgsBuilder.optionIfNonEmpty("-d", " "), []) + XCTAssertEqual(ArgsBuilder.optionIfNonEmpty("-d", " \t\n "), []) } - @Test("optionIfNonEmpty: trims surrounding whitespace") - func optionIfNonEmptyTrims() { - #expect(ArgsBuilder.optionIfNonEmpty("-d", " label ") == ["-d", "label"]) - #expect(ArgsBuilder.optionIfNonEmpty("-d", "\tcal.cal\n") == ["-d", "cal.cal"]) + func testOptionIfNonEmptyTrims() { + XCTAssertEqual(ArgsBuilder.optionIfNonEmpty("-d", " label "), ["-d", "label"]) + XCTAssertEqual(ArgsBuilder.optionIfNonEmpty("-d", "\tcal.cal\n"), ["-d", "cal.cal"]) } // MARK: - optionUnlessApprox - @Test("optionUnlessApprox: nil emits nothing") - func optionUnlessApproxNil() { - #expect(ArgsBuilder.optionUnlessApprox("-N", nil, skip: 0.50) == []) + func testOptionUnlessApproxNil() { + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-N", nil, skip: 0.50), []) } - @Test("optionUnlessApprox: exact skip value emits nothing") - func optionUnlessApproxExactSkip() { - #expect(ArgsBuilder.optionUnlessApprox("-N", 0.50, skip: 0.50) == []) - #expect(ArgsBuilder.optionUnlessApprox("-V", 1.0, skip: 1.0) == []) + func testOptionUnlessApproxExactSkip() { + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-N", 0.50, skip: 0.50), []) + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-V", 1.0, skip: 1.0), []) } - @Test("optionUnlessApprox: within epsilon emits nothing") - func optionUnlessApproxWithinEpsilon() { - #expect(ArgsBuilder.optionUnlessApprox("-N", 0.5005, skip: 0.50) == []) - #expect(ArgsBuilder.optionUnlessApprox("-V", 0.9995, skip: 1.0) == []) + func testOptionUnlessApproxWithinEpsilon() { + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-N", 0.5005, skip: 0.50), []) + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-V", 0.9995, skip: 1.0), []) } - @Test("optionUnlessApprox: outside epsilon emits flag") - func optionUnlessApproxOutsideEpsilon() { - #expect(ArgsBuilder.optionUnlessApprox("-N", 0.75, skip: 0.50) == ["-N", "0.75"]) - #expect(ArgsBuilder.optionUnlessApprox("-V", 1.50, skip: 1.0) == ["-V", "1.50"]) - #expect(ArgsBuilder.optionUnlessApprox("-N", 0.498, skip: 0.50) == ["-N", "0.50"]) + func testOptionUnlessApproxOutsideEpsilon() { + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-N", 0.75, skip: 0.50), ["-N", "0.75"]) + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-V", 1.50, skip: 1.0), ["-V", "1.50"]) + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-N", 0.498, skip: 0.50), ["-N", "0.50"]) } - @Test("optionUnlessApprox: POSIX formatting is locale-stable") - func optionUnlessApproxPOSIX() { + func testOptionUnlessApproxPOSIX() { // 1234.5 must never produce a grouping separator or comma decimal. - #expect(ArgsBuilder.optionUnlessApprox("-p", 1234.5, skip: 1.0) == ["-p", "1234.50"]) - #expect(ArgsBuilder.optionUnlessApprox("-p", 2.0, skip: 1.0) == ["-p", "2.00"]) + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-p", 1234.5, skip: 1.0), ["-p", "1234.50"]) + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-p", 2.0, skip: 1.0), ["-p", "2.00"]) } - @Test("optionUnlessApprox: custom epsilon and format honoured") - func optionUnlessApproxCustom() { - #expect(ArgsBuilder.optionUnlessApprox("-x", 1.005, skip: 1.0, epsilon: 0.01) == []) - #expect(ArgsBuilder.optionUnlessApprox("-x", 1.5, skip: 1.0, format: "%.1f") == ["-x", "1.5"]) + func testOptionUnlessApproxCustom() { + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-x", 1.005, skip: 1.0, epsilon: 0.01), []) + XCTAssertEqual(ArgsBuilder.optionUnlessApprox("-x", 1.5, skip: 1.0, format: "%.1f"), ["-x", "1.5"]) } // MARK: - flag - @Test("flag: true emits the bare flag") - func flagTrue() { - #expect(ArgsBuilder.flag("-G", when: true) == ["-G"]) - #expect(ArgsBuilder.flag("-r", when: true) == ["-r"]) + func testFlagTrue() { + XCTAssertEqual(ArgsBuilder.flag("-G", when: true), ["-G"]) + XCTAssertEqual(ArgsBuilder.flag("-r", when: true), ["-r"]) } - @Test("flag: false emits nothing") - func flagFalse() { - #expect(ArgsBuilder.flag("-G", when: false) == []) - #expect(ArgsBuilder.flag("-r", when: false) == []) + func testFlagFalse() { + XCTAssertEqual(ArgsBuilder.flag("-G", when: false), []) + XCTAssertEqual(ArgsBuilder.flag("-r", when: false), []) } } diff --git a/Tests/ICCeryCoreTests/ArtefactFilesTests.swift b/Tests/ICCeryCoreTests/ArtefactFilesTests.swift index aa85368..f5c7346 100644 --- a/Tests/ICCeryCoreTests/ArtefactFilesTests.swift +++ b/Tests/ICCeryCoreTests/ArtefactFilesTests.swift @@ -1,4 +1,4 @@ -import Testing +import XCTest import Foundation import ImageIO import UniformTypeIdentifiers @@ -10,9 +10,8 @@ private func tempURL(_ name: String) -> URL { .appendingPathComponent(name) } -@Suite("Ti2Header") -struct Ti2HeaderTests { - @Test func parsesKeywordsAndSibling() throws { +final class Ti2HeaderTests: XCTestCase { + func testParsesKeywordsAndSibling() throws { let dir = FileManager.default.temporaryDirectory .appendingPathComponent("iccery-ti2-\(UUID().uuidString)") try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) @@ -31,18 +30,18 @@ struct Ti2HeaderTests { ) let h = Ti2Header.parse(dir.appendingPathComponent("job.ti2")) - #expect(h.instrument == "i1iO") - #expect(h.patchCount == 800) - #expect(h.pageCount == 3) - #expect(h.hasSiblingTi1) + XCTAssertEqual(h.instrument, "i1iO") + XCTAssertEqual(h.patchCount, 800) + XCTAssertEqual(h.pageCount, 3) + XCTAssertTrue(h.hasSiblingTi1) } - @Test func missingFileYieldsEmptyHeader() { + func testMissingFileYieldsEmptyHeader() { let h = Ti2Header.parse(URL(fileURLWithPath: "/nonexistent/x.ti2")) - #expect(h.instrument == nil && h.patchCount == nil && !h.hasSiblingTi1) + XCTAssertTrue(h.instrument == nil && h.patchCount == nil && !h.hasSiblingTi1) } - @Test func numberOfFieldsIsNotPatchCount() throws { + func testNumberOfFieldsIsNotPatchCount() throws { let url = tempURL("t.ti2") try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true @@ -50,12 +49,11 @@ struct Ti2HeaderTests { try "NUMBER_OF_FIELDS 9\nNUMBER_OF_SETS 52\nBEGIN_DATA\n".write( to: url, atomically: true, encoding: .utf8 ) - #expect(Ti2Header.parse(url).patchCount == 52) + XCTAssertEqual(Ti2Header.parse(url).patchCount, 52) } } -@Suite("TiffPreview") -struct TiffPreviewTests { +final class TiffPreviewTests: XCTestCase { /// Builds a real 2000×1000 TIFF in a temp dir via ImageIO. private func makeTiff(width: Int = 2000, height: Int = 1000) throws -> URL { let url = tempURL("big.tif") @@ -81,50 +79,48 @@ struct TiffPreviewTests { return url } - @Test func producesCappedPNG() throws { + func testProducesCappedPNG() throws { let tiff = try makeTiff() let png = TiffPreview.previewPNG(tiff: tiff) - #expect(png != nil) + XCTAssertNotNil(png) // PNG magic - #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])) // Verify the cap by decoding the thumbnail header. let src = CGImageSourceCreateWithData(png! as CFData, nil)! let img = CGImageSourceCreateImageAtIndex(src, 0, nil)! - #expect(max(img.width, img.height) <= TiffPreview.maxEdge) - #expect(img.width == 1200) + XCTAssertTrue(max(img.width, img.height) <= TiffPreview.maxEdge) + XCTAssertEqual(img.width, 1200) } - @Test func nonTiffReturnsNil() throws { + func testNonTiffReturnsNil() throws { let url = tempURL("not-tiff.txt") try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true ) try "hello".write(to: url, atomically: true, encoding: .utf8) - #expect(TiffPreview.previewPNG(tiff: url) == nil) + XCTAssertNil(TiffPreview.previewPNG(tiff: url)) } } -@Suite("ArtefactFiles") -struct ArtefactFilesTests { - @Test func base64RoundTrip() throws { +final class ArtefactFilesTests: XCTestCase { + func testBase64RoundTrip() throws { let url = tempURL("a.txt") try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true ) try "hello".write(to: url, atomically: true, encoding: .utf8) let b64 = try ArtefactFiles.readBase64(url) - #expect(Data(base64Encoded: b64) == Data("hello".utf8)) + XCTAssertEqual(Data(base64Encoded: b64), Data("hello".utf8)) } - @Test func defaultWorkingDirExists() { - #expect(FileManager.default.fileExists( + func testDefaultWorkingDirExists() { + XCTAssertTrue(FileManager.default.fileExists( atPath: ArtefactFiles.defaultWorkingDirectory().path )) } } -@Suite("ArtefactProbe profile resolve") -struct ArtefactProbeProfileTests { +final class ArtefactProbeProfileTests: XCTestCase { private func makeDir() throws -> URL { let dir = FileManager.default.temporaryDirectory .appendingPathComponent("probe-\(UUID().uuidString)") @@ -134,93 +130,83 @@ struct ArtefactProbeProfileTests { // MARK: Basename probe matrix (#69) - @Test("basename probe: only .icc exists") - func onlyIcc() throws { + func testOnlyIcc() throws { let dir = try makeDir() let icc = dir.appendingPathComponent("job.icc") try Data("icc".utf8).write(to: icc) - #expect(ArtefactProbe.resolveProfile(basename: "job", cwd: dir)?.path == icc.path) + XCTAssertEqual(ArtefactProbe.resolveProfile(basename: "job", cwd: dir)?.path, icc.path) } - @Test("basename probe: only .icm exists") - func onlyIcm() throws { + func testOnlyIcm() throws { let dir = try makeDir() let icm = dir.appendingPathComponent("job.icm") try Data("icm".utf8).write(to: icm) - #expect(ArtefactProbe.resolveProfile(basename: "job", cwd: dir)?.path == icm.path) + XCTAssertEqual(ArtefactProbe.resolveProfile(basename: "job", cwd: dir)?.path, icm.path) } - @Test("basename probe prefers .icm") - func icmWins() throws { + func testIcmWins() throws { let dir = try makeDir() try Data("icc".utf8).write(to: dir.appendingPathComponent("job.icc")) let icm = dir.appendingPathComponent("job.icm") try Data("icm".utf8).write(to: icm) let url = ArtefactProbe.resolveProfile(basename: "job", cwd: dir) - #expect(url?.path == icm.path) + XCTAssertEqual(url?.path, icm.path) } - @Test("basename probe: neither exists returns nil") - func neitherExists() throws { + func testNeitherExists() throws { let dir = try makeDir() - #expect(ArtefactProbe.resolveProfile(basename: "job", cwd: dir) == nil) + XCTAssertNil(ArtefactProbe.resolveProfile(basename: "job", cwd: dir)) } // MARK: Explicit URL matrix (#69 / #83) - @Test("explicit existing .icc wins even when .icm exists") - func explicitIccWins() throws { + func testExplicitIccWins() throws { let dir = try makeDir() let icc = dir.appendingPathComponent("job.icc") try Data("icc".utf8).write(to: icc) try Data("icm".utf8).write(to: dir.appendingPathComponent("job.icm")) - #expect(ArtefactProbe.resolveProfile(icc).path == icc.path) + XCTAssertEqual(ArtefactProbe.resolveProfile(icc).path, icc.path) } - @Test("explicit existing .icm wins even when .icc exists") - func explicitIcmWins() throws { + func testExplicitIcmWins() throws { let dir = try makeDir() try Data("icc".utf8).write(to: dir.appendingPathComponent("job.icc")) let icm = dir.appendingPathComponent("job.icm") try Data("icm".utf8).write(to: icm) - #expect(ArtefactProbe.resolveProfile(icm).path == icm.path) + XCTAssertEqual(ArtefactProbe.resolveProfile(icm).path, icm.path) } - @Test("explicit missing .icc flips to sibling .icm") - func flipExtension() throws { + func testFlipExtension() throws { let dir = try makeDir() let icc = dir.appendingPathComponent("job.icc") let icm = dir.appendingPathComponent("job.icm") try Data("icm".utf8).write(to: icm) let resolved = ArtefactProbe.resolveProfile(icc) - #expect(resolved.path == icm.path) + XCTAssertEqual(resolved.path, icm.path) } - @Test("explicit missing .icm flips to sibling .icc") - func flipToIcc() throws { + func testFlipToIcc() throws { let dir = try makeDir() let icc = dir.appendingPathComponent("job.icc") let icm = dir.appendingPathComponent("job.icm") try Data("icc".utf8).write(to: icc) - #expect(ArtefactProbe.resolveProfile(icm).path == icc.path) + XCTAssertEqual(ArtefactProbe.resolveProfile(icm).path, icc.path) } - @Test("explicit missing both returns the original URL") - func missingBoth() throws { + func testMissingBoth() throws { let dir = try makeDir() let icc = dir.appendingPathComponent("job.icc") - #expect(ArtefactProbe.resolveProfile(icc).path == icc.path) + XCTAssertEqual(ArtefactProbe.resolveProfile(icc).path, icc.path) } - @Test("unrelated extension is never rewritten") - func unrelatedExtension() throws { + func testUnrelatedExtension() throws { let dir = try makeDir() let mpp = dir.appendingPathComponent("job.mpp") let icc = dir.appendingPathComponent("job.icc") try Data("icc".utf8).write(to: icc) // Even though a sibling .icc exists, a missing .mpp stays .mpp. - #expect(ArtefactProbe.resolveProfile(mpp).path == mpp.path) + XCTAssertEqual(ArtefactProbe.resolveProfile(mpp).path, mpp.path) let txt = dir.appendingPathComponent("job.txt") - #expect(ArtefactProbe.resolveProfile(txt).path == txt.path) + XCTAssertEqual(ArtefactProbe.resolveProfile(txt).path, txt.path) } } diff --git a/Tests/ICCeryCoreTests/BinaryResolverTests.swift b/Tests/ICCeryCoreTests/BinaryResolverTests.swift index c1c7c76..06c1c5f 100644 --- a/Tests/ICCeryCoreTests/BinaryResolverTests.swift +++ b/Tests/ICCeryCoreTests/BinaryResolverTests.swift @@ -1,9 +1,8 @@ -import Testing +import XCTest import Foundation @testable import ICCeryCore -@Suite("BinaryResolver") -struct BinaryResolverTests { +final class BinaryResolverTests: XCTestCase { private func makeTree(_ body: (URL) throws -> Void) throws -> URL { let root = FileManager.default.temporaryDirectory @@ -22,16 +21,16 @@ struct BinaryResolverTests { } } - @Test func overrideDirWinsWhenFileExists() throws { + func testOverrideDirWinsWhenFileExists() throws { let override = try makeTree { root in try touch(root.appendingPathComponent("targen")) } let bundled = try makeTree { _ in } let r = BinaryResolver(bundledRoot: bundled, overrideDir: override) - #expect(r.resolve("targen") == override.appendingPathComponent("targen")) + XCTAssertEqual(r.resolve("targen"), override.appendingPathComponent("targen")) } - @Test func overrideFallsThroughWhenMissing() throws { + func testOverrideFallsThroughWhenMissing() throws { let override = try makeTree { _ in } let bundled = try makeTree { root in let dir = root.appendingPathComponent("macos-universal") @@ -39,10 +38,10 @@ struct BinaryResolverTests { try touch(dir.appendingPathComponent("instlist")) } let r = BinaryResolver(bundledRoot: bundled, overrideDir: override) - #expect(r.resolve("targen").path.contains("macos-universal/targen")) + XCTAssertTrue(r.resolve("targen").path.contains("macos-universal/targen")) } - @Test func universalPreferredWhenMarkerPresent() throws { + func testUniversalPreferredWhenMarkerPresent() throws { let bundled = try makeTree { root in for dir in ["macos-universal", "macos-x86_64"] { let d = root.appendingPathComponent(dir) @@ -51,10 +50,10 @@ struct BinaryResolverTests { } } let r = BinaryResolver(bundledRoot: bundled) - #expect(r.platformDir() == "macos-universal") + XCTAssertEqual(r.platformDir(), "macos-universal") } - @Test func fallsBackToArchDir() throws { + func testFallsBackToArchDir() throws { let bundled = try makeTree { root in let d = root.appendingPathComponent("macos-x86_64") try FileManager.default.createDirectory(at: d, withIntermediateDirectories: true) @@ -64,20 +63,20 @@ struct BinaryResolverTests { bundledRoot: bundled, archDirs: ["macos-universal", "macos-x86_64"] ) - #expect(r.platformDir() == "macos-x86_64") + XCTAssertEqual(r.platformDir(), "macos-x86_64") } - @Test func missingEverythingReturnsConstructedPath() throws { + func testMissingEverythingReturnsConstructedPath() throws { let bundled = try makeTree { _ in } let r = BinaryResolver(bundledRoot: bundled) // v1 semantic: path is returned; spawn surfaces the error. - #expect(r.resolve("targen").path.hasSuffix("macos-universal/targen")) - #expect(!r.exists(r.resolve("targen"))) + XCTAssertTrue(r.resolve("targen").path.hasSuffix("macos-universal/targen")) + XCTAssertFalse(r.exists(r.resolve("targen"))) } - @Test func mockAndGamutPaths() throws { + func testMockAndGamutPaths() throws { let r = BinaryResolver(bundledRoot: URL(fileURLWithPath: "/x")) - #expect(r.mock("chartread").path == "/x/mocks/chartread.mock") - #expect(r.referenceGamut("sRGB.gam").path == "/x/reference_gamuts/sRGB.gam") + XCTAssertEqual(r.mock("chartread").path, "/x/mocks/chartread.mock") + XCTAssertEqual(r.referenceGamut("sRGB.gam").path, "/x/reference_gamuts/sRGB.gam") } } diff --git a/Tests/ICCeryCoreTests/CGATSParserTests.swift b/Tests/ICCeryCoreTests/CGATSParserTests.swift index a5e9a74..8eb48d4 100644 --- a/Tests/ICCeryCoreTests/CGATSParserTests.swift +++ b/Tests/ICCeryCoreTests/CGATSParserTests.swift @@ -1,9 +1,8 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("CGATS Parser & Writer") -struct CGATSParserTests { +final class CGATSParserTests: XCTestCase { private static let canonicalCTI3 = """ CTI3 @@ -21,44 +20,40 @@ struct CGATSParserTests { END_DATA """ - @Test("Parses CTI3 with canonical field names") - func parseCTI3() throws { + func testParseCTI3() throws { let dataset = try CGATSParser.parse(Self.canonicalCTI3) - #expect(dataset.format == .cti3) - #expect(dataset.samples.count == 2) - #expect(dataset.colorRep == "RGB") - #expect(dataset.deviceClass == "DISPLAY") - #expect(dataset.samples[0].id == "1") - #expect(dataset.samples[0].loc == "A1") - #expect(dataset.samples[1].values["RGB_G"] == "50.0000") + XCTAssertEqual(dataset.format, .cti3) + XCTAssertEqual(dataset.samples.count, 2) + XCTAssertEqual(dataset.colorRep, "RGB") + XCTAssertEqual(dataset.deviceClass, "DISPLAY") + XCTAssertEqual(dataset.samples[0].id, "1") + XCTAssertEqual(dataset.samples[0].loc, "A1") + XCTAssertEqual(dataset.samples[1].values["RGB_G"], "50.0000") } - @Test("Round-trips parse, write, reparse") - func roundTrip() throws { + func testRoundTrip() throws { let first = try CGATSParser.parse(Self.canonicalCTI3) let text = try CGATSWriter.write(first) let second = try CGATSParser.parse(text) - #expect(second.format == first.format) - #expect(second.samples.count == first.samples.count) - #expect(second.colorRep == first.colorRep) - #expect(second.deviceClass == first.deviceClass) + XCTAssertEqual(second.format, first.format) + XCTAssertEqual(second.samples.count, first.samples.count) + XCTAssertEqual(second.colorRep, first.colorRep) + XCTAssertEqual(second.deviceClass, first.deviceClass) } - @Test("Parses CSV with comma delimiters") - func parseCSV() throws { + func testParseCSV() throws { let csv = """ SAMPLE_ID,SAMPLE_LOC,RGB_R,RGB_G,RGB_B,XYZ_X,XYZ_Y,XYZ_Z,LAB_L,LAB_A,LAB_B 1,A1,50,0,0,20,10,5,50,60,30 2,A2,0,50,0,10,30,5,60,-50,40 """ let dataset = try CGATSParser.parse(csv, sourceURL: URL(fileURLWithPath: "/tmp/sample.csv")) - #expect(dataset.format == .csv) - #expect(dataset.samples.count == 2) - #expect(dataset.samples[0].values["RGB_R"] == "50.0000") + XCTAssertEqual(dataset.format, .csv) + XCTAssertEqual(dataset.samples.count, 2) + XCTAssertEqual(dataset.samples[0].values["RGB_R"], "50.0000") } - @Test("Converts 0-255 device values to 0-100") - func converts255To100() throws { + func testConverts255To100() throws { let rgb = """ CTI3 COLOR_REP RGB @@ -72,12 +67,11 @@ struct CGATSParserTests { END_DATA """ let dataset = try CGATSParser.parse(rgb) - #expect(dataset.samples[0].values["RGB_R"] == "100.0000") - #expect(dataset.samples[0].values["RGB_G"] == "50.1961") + XCTAssertEqual(dataset.samples[0].values["RGB_R"], "100.0000") + XCTAssertEqual(dataset.samples[0].values["RGB_G"], "50.1961") } - @Test("Synthesizes COLOR_REP and DEVICE_CLASS when missing") - func synthesizesMetadata() throws { + func testSynthesizesMetadata() throws { let cmyk = """ CTI3 NUMBER_OF_FIELDS 6 @@ -90,19 +84,15 @@ struct CGATSParserTests { END_DATA """ let dataset = try CGATSParser.parse(cmyk) - #expect(dataset.colorRep == "CMYK") - #expect(dataset.deviceClass == "PRINTER") + XCTAssertEqual(dataset.colorRep, "CMYK") + XCTAssertEqual(dataset.deviceClass, "PRINTER") } - @Test("Rejects empty file") - func rejectsEmpty() { - #expect(throws: (any Error).self) { - _ = try CGATSParser.parse("") - } + func testRejectsEmpty() { + XCTAssertThrowsError(try CGATSParser.parse("")) } - @Test("Rejects malformed arity") - func rejectsArity() { + func testRejectsArity() { let bad = """ CTI3 NUMBER_OF_FIELDS 2 @@ -114,21 +104,18 @@ struct CGATSParserTests { 1 END_DATA """ - #expect(throws: (any Error).self) { - _ = try CGATSParser.parse(bad) - } + XCTAssertThrowsError(try CGATSParser.parse(bad)) } - @Test("Writer emits valid .ti3 with tabs and required keywords") - func writerFormat() throws { + func testWriterFormat() throws { let dataset = try CGATSParser.parse(Self.canonicalCTI3) let text = try CGATSWriter.write(dataset) - #expect(text.contains("CTI3")) - #expect(text.contains("BEGIN_DATA_FORMAT")) - #expect(text.contains("BEGIN_DATA")) - #expect(text.contains("END_DATA")) - #expect(text.contains("COLOR_REP")) - #expect(text.contains("DEVICE_CLASS")) - #expect(text.contains("\t")) + XCTAssertTrue(text.contains("CTI3")) + XCTAssertTrue(text.contains("BEGIN_DATA_FORMAT")) + XCTAssertTrue(text.contains("BEGIN_DATA")) + XCTAssertTrue(text.contains("END_DATA")) + XCTAssertTrue(text.contains("COLOR_REP")) + XCTAssertTrue(text.contains("DEVICE_CLASS")) + XCTAssertTrue(text.contains("\t")) } } diff --git a/Tests/ICCeryCoreTests/CalibrationIdentityTests.swift b/Tests/ICCeryCoreTests/CalibrationIdentityTests.swift index 4422e87..2f2da4b 100644 --- a/Tests/ICCeryCoreTests/CalibrationIdentityTests.swift +++ b/Tests/ICCeryCoreTests/CalibrationIdentityTests.swift @@ -1,78 +1,67 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore /// Issue #83 — canonical `CAL_` / original-stem pairing. -@Suite("CalibrationIdentity") -struct CalibrationIdentityTests { - @Test("live foo, no persisted") - func livePlain() { +final class CalibrationIdentityTests: XCTestCase { + func testLivePlain() { let id = CalibrationIdentity.parse(liveBasename: "foo", persistedOriginal: "") - #expect(id.originalBasename == "foo") - #expect(id.calibrationBasename == "CAL_foo") + XCTAssertEqual(id.originalBasename, "foo") + XCTAssertEqual(id.calibrationBasename, "CAL_foo") } - @Test("live foo ignores stale persisted") - func livePlainIgnoresPersisted() { + func testLivePlainIgnoresPersisted() { let id = CalibrationIdentity.parse(liveBasename: "foo", persistedOriginal: "bar") - #expect(id.originalBasename == "foo") - #expect(id.calibrationBasename == "CAL_foo") + XCTAssertEqual(id.originalBasename, "foo") + XCTAssertEqual(id.calibrationBasename, "CAL_foo") } - @Test("live CAL_foo, persisted foo") - func liveCalPersisted() { + func testLiveCalPersisted() { let id = CalibrationIdentity.parse(liveBasename: "CAL_foo", persistedOriginal: "foo") - #expect(id.originalBasename == "foo") - #expect(id.calibrationBasename == "CAL_foo") + XCTAssertEqual(id.originalBasename, "foo") + XCTAssertEqual(id.calibrationBasename, "CAL_foo") } - @Test("live CAL_foo, empty persisted strips prefix") - func liveCalNoPersist() { + func testLiveCalNoPersist() { let id = CalibrationIdentity.parse(liveBasename: "CAL_foo", persistedOriginal: "") - #expect(id.originalBasename == "foo") - #expect(id.calibrationBasename == "CAL_foo") + XCTAssertEqual(id.originalBasename, "foo") + XCTAssertEqual(id.calibrationBasename, "CAL_foo") } - @Test("persisted original wins over CAL_ live") - func persistedWins() { + func testPersistedWins() { let id = CalibrationIdentity.parse(liveBasename: "CAL_foo", persistedOriginal: "bar") - #expect(id.originalBasename == "bar") - #expect(id.calibrationBasename == "CAL_bar") + XCTAssertEqual(id.originalBasename, "bar") + XCTAssertEqual(id.calibrationBasename, "CAL_bar") } - @Test("empty live yields empty identity even with persisted original") - func emptyLiveWithPersisted() { + func testEmptyLiveWithPersisted() { let id = CalibrationIdentity.parse(liveBasename: "", persistedOriginal: "foo") - #expect(id.originalBasename.isEmpty) - #expect(id.calibrationBasename.isEmpty) + XCTAssertTrue(id.originalBasename.isEmpty) + XCTAssertTrue(id.calibrationBasename.isEmpty) } - @Test("empty live, empty persisted") - func emptyLive() { + func testEmptyLive() { let id = CalibrationIdentity.parse(liveBasename: "", persistedOriginal: "") - #expect(id.originalBasename.isEmpty) - #expect(id.calibrationBasename.isEmpty) + XCTAssertTrue(id.originalBasename.isEmpty) + XCTAssertTrue(id.calibrationBasename.isEmpty) } - @Test("prefix is idempotent on already-prefixed input") - func alreadyPrefixed() { - #expect(CalibrationIdentity.prefix("CAL_foo") == "CAL_foo") - #expect(CalibrationIdentity.prefix("foo") == "CAL_foo") + func testAlreadyPrefixed() { + XCTAssertEqual(CalibrationIdentity.prefix("CAL_foo"), "CAL_foo") + XCTAssertEqual(CalibrationIdentity.prefix("foo"), "CAL_foo") let id = CalibrationIdentity.parse(liveBasename: "CAL_CAL_foo", persistedOriginal: "") - #expect(id.originalBasename == "CAL_foo") - #expect(id.calibrationBasename == "CAL_foo") + XCTAssertEqual(id.originalBasename, "CAL_foo") + XCTAssertEqual(id.calibrationBasename, "CAL_foo") } - @Test("prefix never invents a name from empty input") - func prefixEmpty() { - #expect(CalibrationIdentity.prefix("").isEmpty) - #expect(CalibrationIdentity.strip("foo") == "foo") - #expect(CalibrationIdentity.strip("CAL_foo") == "foo") + func testPrefixEmpty() { + XCTAssertTrue(CalibrationIdentity.prefix("").isEmpty) + XCTAssertEqual(CalibrationIdentity.strip("foo"), "foo") + XCTAssertEqual(CalibrationIdentity.strip("CAL_foo"), "foo") } - @Test("runner process id for a calibration targen is targen_CAL_*") - func processIdMatches() { + func testProcessIdMatches() { let cal = CalibrationIdentity.prefix("foo") - #expect(ProcessID.targen(cal) == "targen_CAL_foo") + XCTAssertEqual(ProcessID.targen(cal), "targen_CAL_foo") } } diff --git a/Tests/ICCeryCoreTests/CalibrationTargenArgsTests.swift b/Tests/ICCeryCoreTests/CalibrationTargenArgsTests.swift index c43313a..1739444 100644 --- a/Tests/ICCeryCoreTests/CalibrationTargenArgsTests.swift +++ b/Tests/ICCeryCoreTests/CalibrationTargenArgsTests.swift @@ -1,12 +1,10 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("CalibrationTargenArgs") -struct CalibrationTargenArgsTests { +final class CalibrationTargenArgsTests: XCTestCase { - @Test("RGB baseline") - func rgbBaseline() throws { + func testRgbBaseline() throws { let config = CalibrationTargenConfig( colourSpace: .rgb, steps: 21, @@ -15,11 +13,10 @@ struct CalibrationTargenArgsTests { workingDirectory: URL(fileURLWithPath: "/tmp") ) let args = try CalibrationTargenArgs.build(config: config) - #expect(args == ["-v", "-d", "2", "-s", "21", "-g", "21", "-e", "4", "-f", "0", "CAL_demo"]) + XCTAssertEqual(args, ["-v", "-d", "2", "-s", "21", "-g", "21", "-e", "4", "-f", "0", "CAL_demo"]) } - @Test("CMYK baseline with ink limit and neutral emphasis") - func cmykWithOptions() throws { + func testCmykWithOptions() throws { let config = CalibrationTargenConfig( colourSpace: .cmyk, steps: 25, @@ -30,33 +27,26 @@ struct CalibrationTargenArgsTests { workingDirectory: URL(fileURLWithPath: "/tmp") ) let args = try CalibrationTargenArgs.build(config: config) - #expect(args == ["-v", "-d", "4", "-s", "25", "-g", "25", "-e", "4", "-f", "0", "-n", "25", "-l", "320", "CAL_printer"]) + XCTAssertEqual(args, ["-v", "-d", "4", "-s", "25", "-g", "25", "-e", "4", "-f", "0", "-n", "25", "-l", "320", "CAL_printer"]) } - @Test("Rejects out-of-range steps") - func rejectsBadSteps() { + func testRejectsBadSteps() { let config = CalibrationTargenConfig(steps: 5, basename: "demo") - #expect(throws: (any Error).self) { - _ = try CalibrationTargenArgs.build(config: config) - } + XCTAssertThrowsError(try CalibrationTargenArgs.build(config: config)) } - @Test("Rejects bad CMYK ink limit") - func rejectsBadInkLimit() { + func testRejectsBadInkLimit() { let config = CalibrationTargenConfig( colourSpace: .cmyk, inkLimit: 500, basename: "demo" ) - #expect(throws: (any Error).self) { - _ = try CalibrationTargenArgs.build(config: config) - } + XCTAssertThrowsError(try CalibrationTargenArgs.build(config: config)) } - @Test("Does not double-prefix an existing CAL_ basename") - func noDoublePrefix() throws { + func testNoDoublePrefix() throws { let config = CalibrationTargenConfig(basename: "CAL_test") let args = try CalibrationTargenArgs.build(config: config) - #expect(args.last == "CAL_test") + XCTAssertEqual(args.last, "CAL_test") } } diff --git a/Tests/ICCeryCoreTests/ColprofArgsTests.swift b/Tests/ICCeryCoreTests/ColprofArgsTests.swift index 9b4f0f7..653e674 100644 --- a/Tests/ICCeryCoreTests/ColprofArgsTests.swift +++ b/Tests/ICCeryCoreTests/ColprofArgsTests.swift @@ -1,72 +1,63 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("ColprofArgs") -struct ColprofArgsTests { +final class ColprofArgsTests: XCTestCase { - @Test("Default algorithm and quality") - func defaults() throws { + func testDefaults() throws { let config = ColprofConfig(basename: "target") let args = try ColprofArgs.build(config: config) - #expect(args == ["-v", "-a", "l", "-q", "m", "target"]) + XCTAssertEqual(args, ["-v", "-a", "l", "-q", "m", "target"]) } - @Test("FWA bare -f when empty string") - func fwaBareFlag() throws { + func testFwaBareFlag() throws { let config = ColprofConfig(fwa: "", basename: "target") let args = try ColprofArgs.build(config: config) - #expect(args == ["-v", "-a", "l", "-q", "m", "-f", "target"]) + XCTAssertEqual(args, ["-v", "-a", "l", "-q", "m", "-f", "target"]) } - @Test("FWA D50 and D65 emit -f value") - func fwaD50() throws { + func testFwaD50() throws { let config = ColprofConfig(fwa: "D50", basename: "target") let args = try ColprofArgs.build(config: config) - #expect(args.contains("-f")) - #expect(args.contains("D50")) - #expect(args.last == "target") + XCTAssertTrue(args.contains("-f")) + XCTAssertTrue(args.contains("D50")) + XCTAssertEqual(args.last, "target") } - @Test("FWA none is omitted") - func fwaNoneOmitted() throws { + func testFwaNoneOmitted() throws { let config = ColprofConfig(fwa: "none", basename: "target") let args = try ColprofArgs.build(config: config) - #expect(!args.contains("-f")) + XCTAssertFalse(args.contains("-f")) } - @Test("Viewing conditions skip none") - func viewingCondNoneSkipped() throws { + func testViewingCondNoneSkipped() throws { let config = ColprofConfig( inputViewingCond: "none", outputViewingCond: "mt", basename: "target" ) let args = try ColprofArgs.build(config: config) - #expect(!args.contains("-c")) - #expect(args.contains("-d")) - #expect(args.contains("mt")) + XCTAssertFalse(args.contains("-c")) + XCTAssertTrue(args.contains("-d")) + XCTAssertTrue(args.contains("mt")) } - @Test("Description falls back to basename when empty") - func descriptionFallback() throws { + func testDescriptionFallback() throws { let config = ColprofConfig(description: "", basename: "target") let args = try ColprofArgs.build(config: config) - #expect(!args.contains("-D")) + XCTAssertFalse(args.contains("-D")) } - @Test("Copyright only when non-empty") - func copyright() throws { + func testCopyright() throws { let config = ColprofConfig(copyright: "Gronod 2026", basename: "target") let args = try ColprofArgs.build(config: config) - #expect(args.contains("-C")) - #expect(args.contains("Gronod 2026")) + XCTAssertTrue(args.contains("-C")) + XCTAssertTrue(args.contains("Gronod 2026")) } - @Test("No -u passed") - func noProgressJsonFlag() throws { + func testNoProgressJsonFlag() throws { let config = ColprofConfig(basename: "target") let args = try ColprofArgs.build(config: config) - #expect(!args.contains("-u")) + XCTAssertFalse(args.contains("-u")) } } diff --git a/Tests/ICCeryCoreTests/ColprofProgressTests.swift b/Tests/ICCeryCoreTests/ColprofProgressTests.swift index 922cf53..66f7b31 100644 --- a/Tests/ICCeryCoreTests/ColprofProgressTests.swift +++ b/Tests/ICCeryCoreTests/ColprofProgressTests.swift @@ -1,24 +1,20 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("ColprofProgress") -struct ColprofProgressTests { +final class ColprofProgressTests: XCTestCase { - @Test("Classifies gamut mapping") - func gamutMapping() { - #expect(ColprofProgressClassifier.classify(line: "Gamut mapping calculation in progress") == .gamutMapping) + func testGamutMapping() { + XCTAssertEqual(ColprofProgressClassifier.classify(line: "Gamut mapping calculation in progress"), .gamutMapping) } - @Test("Classifies fitting or clut") - func fitting() { - #expect(ColprofProgressClassifier.classify(line: "Fitting cLUT grid points") == .fittingClut) - #expect(ColprofProgressClassifier.classify(line: "clut table") == .fittingClut) + func testFitting() { + XCTAssertEqual(ColprofProgressClassifier.classify(line: "Fitting cLUT grid points"), .fittingClut) + XCTAssertEqual(ColprofProgressClassifier.classify(line: "clut table"), .fittingClut) } - @Test("Classifies writing") - func writing() { - #expect(ColprofProgressClassifier.classify(line: "Writing ICC profile header") == .writingIcc) - #expect(ColprofProgressClassifier.classify(line: "icc profile written") == .writingIcc) + func testWriting() { + XCTAssertEqual(ColprofProgressClassifier.classify(line: "Writing ICC profile header"), .writingIcc) + XCTAssertEqual(ColprofProgressClassifier.classify(line: "icc profile written"), .writingIcc) } } diff --git a/Tests/ICCeryCoreTests/CupsOptionsFilterTests.swift b/Tests/ICCeryCoreTests/CupsOptionsFilterTests.swift index 52498d3..0f035da 100644 --- a/Tests/ICCeryCoreTests/CupsOptionsFilterTests.swift +++ b/Tests/ICCeryCoreTests/CupsOptionsFilterTests.swift @@ -1,4 +1,4 @@ -import Testing +import XCTest import Foundation @testable import ICCeryCore @testable import ICCery @@ -6,48 +6,42 @@ import AppKit import ApplicationServices /// Issue 14 — PMPrintSettingsToOptions capture filter (docs/11 layer ⑥). -@Suite("CupsOptionsFilter") -struct CupsOptionsFilterTests { +final class CupsOptionsFilterTests: XCTestCase { - @Test("Drops com.apple.*, collate, copies, job-sheets, AP_* keys") - func dropsReserved() { + func testDropsReserved() { let raw = "AP_ColorMatchingMode=AP_ApplicationColorMatching " + "AP.ColorMatchingMode=AP_ApplicationColorMatching " + "com.apple.print.JobTicket.PMTotalSidesImaged=0 " + "collate=true copies=1 job-sheets=none,none " + "pserrorhandler-requested=standard " + "MediaType=PhotographicGlossy" - #expect(CupsOptionsFilter.filter(raw) == "MediaType=PhotographicGlossy") + XCTAssertEqual(CupsOptionsFilter.filter(raw), "MediaType=PhotographicGlossy") } - @Test("Keeps relevant driver keys, order preserved") - func keepsRelevant() { + func testKeepsRelevant() { let raw = "InputSlot=Rear PageSize=A4 CNIJIntent2=4 " + "Resolution=600x600dpi Duplex=None" - #expect(CupsOptionsFilter.filter(raw) == raw) + XCTAssertEqual(CupsOptionsFilter.filter(raw), raw) } - @Test("Permissive: unknown non-com.* keys survive") - func keepsUnknown() { + func testKeepsUnknown() { let raw = "VendorFooBar=baz MediaType=Plain" - #expect(CupsOptionsFilter.filter(raw) == raw) + XCTAssertEqual(CupsOptionsFilter.filter(raw), raw) } - @Test("Drops empty keys and values") - func dropsEmpty() { + func testDropsEmpty() { let raw = "=noval MediaType= InputSlot=Rear" // "MediaType=" has an empty value → dropped; "=noval" empty key. - #expect(CupsOptionsFilter.filter(raw) == "InputSlot=Rear") + XCTAssertEqual(CupsOptionsFilter.filter(raw), "InputSlot=Rear") } - @Test("extractMediaType prefers MediaType then EPIJ_Medi") - func extractMedia() { - #expect(CupsParsers.extractMediaType( - fromOptionsString: "MediaType=Photo EPIJ_Medi=1") == "Photo") - #expect(CupsParsers.extractMediaType( - fromOptionsString: "EPIJ_Medi=7") == "7") - #expect(CupsParsers.extractMediaType( - fromOptionsString: "PageSize=A4") == nil) + func testExtractMedia() { + XCTAssertEqual(CupsParsers.extractMediaType( + fromOptionsString: "MediaType=Photo EPIJ_Medi=1"), "Photo") + XCTAssertEqual(CupsParsers.extractMediaType( + fromOptionsString: "EPIJ_Medi=7"), "7") + XCTAssertNil(CupsParsers.extractMediaType( + fromOptionsString: "PageSize=A4")) } } @@ -55,9 +49,8 @@ struct CupsOptionsFilterTests { /// `@convention(c)` closures can't capture, so recording goes through /// a file-scope recorder keyed by global state; no private symbols are /// touched. -@Suite("ColorSyncSuppressor") @MainActor -struct ColorSyncSuppressorTests { +final class ColorSyncSuppressorTests: XCTestCase { /// Fake PMPrintSession — the injected resolver never dereferences it. private var fakeSession: PMPrintSession { @@ -90,55 +83,50 @@ struct ColorSyncSuppressorTests { return s } - @Test("Attempt order: Lock → Mode → NoLock, AP_ prefix first") - func attemptOrder() { + func testAttemptOrder() { Self.recorded = [] Self.succeeding = nil Self.missing = ["PMSessionSetColorMatchingModeLock"] let s = makeSuppressor() - #expect(s.applySPIMode(to: fakeSession) == false) + XCTAssertEqual(s.applySPIMode(to: fakeSession), false) // Lock is unresolvable → skipped; the rest plays out in order. - #expect(Self.recorded.map { "\($0.0)|\($0.1)" } - == ColorMatchingAttempts.attempts + XCTAssertEqual(Self.recorded.map { "\($0.0)|\($0.1)" }, ColorMatchingAttempts.attempts .filter { $0.symbol != "PMSessionSetColorMatchingModeLock" } .map { "\($0.symbol)|\($0.mode)" }) } - @Test("First zero wins — later symbols/modes not called") - func firstZeroWins() { + func testFirstZeroWins() { Self.recorded = [] Self.succeeding = ("PMSessionSetColorMatchingModeLock", "AP_ApplicationColorMatching") Self.missing = [] let s = makeSuppressor() - #expect(s.applySPIMode(to: fakeSession)) - #expect(Self.recorded.map { "\($0.0)|\($0.1)" } == [ + XCTAssertTrue(s.applySPIMode(to: fakeSession)) + XCTAssertEqual(Self.recorded.map { "\($0.0)|\($0.1)" }, [ "PMSessionSetColorMatchingModeLock|AP_ApplicationColorMatching", ]) } - @Test("Mode fallback: AP_ rejected → ApplicationColorMatching tried") - func modeFallback() { + func testModeFallback() { Self.recorded = [] Self.succeeding = ("PMSessionSetColorMatchingModeLock", "ApplicationColorMatching") Self.missing = [] let s = makeSuppressor() - #expect(s.applySPIMode(to: fakeSession)) - #expect(Self.recorded[0].0 == "PMSessionSetColorMatchingModeLock") - #expect(Self.recorded[0].1 == "AP_ApplicationColorMatching") - #expect(Self.recorded[1].0 == "PMSessionSetColorMatchingModeLock") - #expect(Self.recorded[1].1 == "ApplicationColorMatching") - #expect(Self.recorded.count == 2) + XCTAssertTrue(s.applySPIMode(to: fakeSession)) + XCTAssertEqual(Self.recorded[0].0, "PMSessionSetColorMatchingModeLock") + XCTAssertEqual(Self.recorded[0].1, "AP_ApplicationColorMatching") + XCTAssertEqual(Self.recorded[1].0, "PMSessionSetColorMatchingModeLock") + XCTAssertEqual(Self.recorded[1].1, "ApplicationColorMatching") + XCTAssertEqual(Self.recorded.count, 2) } - @Test("All symbols missing → false, no calls") - func allMissing() { + func testAllMissing() { Self.recorded = [] Self.succeeding = nil Self.missing = Set(ColorMatchingAttempts.symbols) let s = makeSuppressor() - #expect(s.applySPIMode(to: fakeSession) == false) - #expect(Self.recorded.isEmpty) + XCTAssertEqual(s.applySPIMode(to: fakeSession), false) + XCTAssertTrue(Self.recorded.isEmpty) } } diff --git a/Tests/ICCeryCoreTests/CupsParserTests.swift b/Tests/ICCeryCoreTests/CupsParserTests.swift index fdf948f..a4be264 100644 --- a/Tests/ICCeryCoreTests/CupsParserTests.swift +++ b/Tests/ICCeryCoreTests/CupsParserTests.swift @@ -1,11 +1,10 @@ -import Testing +import XCTest import Foundation @testable import ICCeryCore /// Issue 12 — CUPS enumeration parsers on recorded fixtures /// (docs/10–11). No live `lpstat`/`lpoptions` is spawned here. -@Suite("CupsParsers") -struct CupsParsersTests { +final class CupsParsersTests: XCTestCase { // Recorded on an Epson XP-55 + Canon Pro9500 host. private let lpstatE = """ @@ -34,112 +33,102 @@ struct CupsParsersTests { cupsPrintQuality/cupsPrintQuality: Draft *Normal High """ - @Test("lpstat -e: one destination per line; empty = success") - func destinations() { - #expect(CupsParsers.lpstatDestinations(lpstatE) == [ + func testDestinations() { + XCTAssertEqual(CupsParsers.lpstatDestinations(lpstatE), [ "Canon_Pro9500_II_series_XPS", "Epson_XP_55_LPD", "EPSON_XP_55_Series", ]) - #expect(CupsParsers.lpstatDestinations("") == []) + XCTAssertEqual(CupsParsers.lpstatDestinations(""), []) } - @Test("lpstat -p: idle / now-printing / disabled statuses") - func statuses() { + func testStatuses() { let s = CupsParsers.lpstatStatuses(lpstatP) - #expect(s["Canon_Pro9500_II_series_XPS"] == .idle) - #expect(s["Epson_XP_55_LPD"] == .printing) - #expect(s["EPSON_XP_55_Series"] == .stopped) + XCTAssertEqual(s["Canon_Pro9500_II_series_XPS"], .idle) + XCTAssertEqual(s["Epson_XP_55_LPD"], .printing) + XCTAssertEqual(s["EPSON_XP_55_Series"], .stopped) } - @Test("lpstat -d: default destination or none") - func defaultDestination() { - #expect(CupsParsers.lpstatDefault( - "system default destination: Canon_Pro9500_II_series_XPS\n") - == "Canon_Pro9500_II_series_XPS") - #expect(CupsParsers.lpstatDefault("no system default destination\n") == nil) + func testDefaultDestination() { + XCTAssertEqual(CupsParsers.lpstatDefault( + "system default destination: Canon_Pro9500_II_series_XPS\n"), "Canon_Pro9500_II_series_XPS") + XCTAssertNil(CupsParsers.lpstatDefault("no system default destination\n")) } - @Test("lpoptions -p: quoted printer-info, bare flags ignored") - func displayName() { - #expect(CupsParsers.lpoptionsDisplayName(lpoptionsP) == "EPSON XP-55 Series") - #expect(CupsParsers.lpoptionsDisplayName("printer-type=42\n") == nil) + func testDisplayName() { + XCTAssertEqual(CupsParsers.lpoptionsDisplayName(lpoptionsP), "EPSON XP-55 Series") + XCTAssertNil(CupsParsers.lpoptionsDisplayName("printer-type=42\n")) } - @Test("lpoptions -l: key/label split, * marks the default") - func optionListings() { + func testOptionListings() { let listings = CupsParsers.lpoptionsList(lpoptionsL) - #expect(listings.count == 6) + XCTAssertEqual(listings.count, 6) let page = listings[0] - #expect(page.key == "PageSize") - #expect(page.label == "Media Size") - #expect(page.defaultChoice == "A4") - #expect(page.choices.contains("Custom.WIDTHxHEIGHT")) - #expect(!page.choices.contains("*A4")) + XCTAssertEqual(page.key, "PageSize") + XCTAssertEqual(page.label, "Media Size") + XCTAssertEqual(page.defaultChoice, "A4") + XCTAssertTrue(page.choices.contains("Custom.WIDTHxHEIGHT")) + XCTAssertFalse(page.choices.contains("*A4")) let slot = listings[1] - #expect(slot.key == "InputSlot") - #expect(slot.choices == ["Auto", "Main", "Photo", "Rear"]) - #expect(slot.defaultChoice == "Main") + XCTAssertEqual(slot.key, "InputSlot") + XCTAssertEqual(slot.choices, ["Auto", "Main", "Photo", "Rear"]) + XCTAssertEqual(slot.defaultChoice, "Main") } - @Test("capabilities: trays/sizes index 1-based, media uses detected key") - func capabilities() { + func testCapabilities() { let service = CupsService() let listings = CupsParsers.lpoptionsList(lpoptionsL) let caps = service.capabilities(from: listings, ppd: nil) - #expect(caps.trays == [ + XCTAssertEqual(caps.trays, [ PrinterTray(id: 1, name: "Auto"), PrinterTray(id: 2, name: "Main"), PrinterTray(id: 3, name: "Photo"), PrinterTray(id: 4, name: "Rear"), ]) - #expect(caps.paperSizes.first == PrinterPaperSize(id: 1, name: "3.5x5")) - #expect(caps.paperSizes.count == 10) - #expect(caps.mediaTypes.map(\.id) == [ + XCTAssertEqual(caps.paperSizes.first, PrinterPaperSize(id: 1, name: "3.5x5")) + XCTAssertEqual(caps.paperSizes.count, 10) + XCTAssertEqual(caps.mediaTypes.map(\.id), [ "Stationery", "PhotographicHighGloss", "Photographic", "PhotographicMatte", "Envelope", ]) - #expect(caps.supportsOrientation) + XCTAssertTrue(caps.supportsOrientation) } - @Test("PPD enrichment maps id → human label") - func ppdLabels() { + func testPpdLabels() { let ppd = """ *CNIJMediaType 42/Photo Paper Plus Semi-gloss: "<>" *CNIJMediaType 0/Plain Paper: "" *en_US.CNIJMediaType 13/Envelope: "" """ let labels = CupsParsers.ppdChoiceLabels(ppd, key: "CNIJMediaType") - #expect(labels["42"] == "Photo Paper Plus Semi-gloss") - #expect(labels["0"] == "Plain Paper") - #expect(labels["13"] == "Envelope") + XCTAssertEqual(labels["42"], "Photo Paper Plus Semi-gloss") + XCTAssertEqual(labels["0"], "Plain Paper") + XCTAssertEqual(labels["13"], "Envelope") } - @Test("detectMediaTypeKey prefers vendor keys in order") - func mediaTypeKey() { - #expect(CupsParsers.detectMediaTypeKey( - optionKeys: ["MediaType", "CNIJMediaType"]) == "CNIJMediaType") - #expect(CupsParsers.detectMediaTypeKey( - optionKeys: ["PageSize", "MediaType"]) == "MediaType") - #expect(CupsParsers.detectMediaTypeKey(optionKeys: ["PageSize"]) == nil) + func testMediaTypeKey() { + XCTAssertEqual(CupsParsers.detectMediaTypeKey( + optionKeys: ["MediaType", "CNIJMediaType"]), "CNIJMediaType") + XCTAssertEqual(CupsParsers.detectMediaTypeKey( + optionKeys: ["PageSize", "MediaType"]), "MediaType") + XCTAssertNil(CupsParsers.detectMediaTypeKey(optionKeys: ["PageSize"])) } - @Test("Driver bypass: Canon Intent2 > Intent; Epson CCor > CMat") - func driverBypass() { + func testDriverBypass() { func pair(_ keys: Set) -> String? { CupsParsers.detectDriverColorBypass(optionKeys: keys) .map { "\($0.key)=\($0.value)" } } - #expect(pair(["CNIJIntent2", "CNIJIntent"]) == "CNIJIntent2=4") - #expect(pair(["CNIJIntent"]) == "CNIJIntent=4") - #expect(pair(["EPIJ_CCor", "EPIJ_CMat"]) == "EPIJ_CCor=0") - #expect(pair(["EPIJ_CMat"]) == "EPIJ_CMat=3") - #expect(pair(["StpColorCorrection"]) == "StpColorCorrection=Uncorrected") - #expect(pair(["ColorCorrection"]) == "ColorCorrection=Uncorrected") - #expect(pair(["EpsonColorMode"]) == "EpsonColorMode=Off") - #expect(pair(["PageSize"]) == nil) + XCTAssertEqual(pair(["CNIJIntent2", "CNIJIntent"]), "CNIJIntent2=4") + XCTAssertEqual(pair(["CNIJIntent"]), "CNIJIntent=4") + XCTAssertEqual(pair(["EPIJ_CCor", "EPIJ_CMat"]), "EPIJ_CCor=0") + XCTAssertEqual(pair(["EPIJ_CMat"]), "EPIJ_CMat=3") + XCTAssertEqual(pair(["StpColorCorrection"]), "StpColorCorrection=Uncorrected") + XCTAssertEqual(pair(["ColorCorrection"]), "ColorCorrection=Uncorrected") + XCTAssertEqual(pair(["EpsonColorMode"]), "EpsonColorMode=Off") + XCTAssertNil(pair(["PageSize"])) } } diff --git a/Tests/ICCeryCoreTests/DriftAlertTests.swift b/Tests/ICCeryCoreTests/DriftAlertTests.swift index 4eb2835..11fb145 100644 --- a/Tests/ICCeryCoreTests/DriftAlertTests.swift +++ b/Tests/ICCeryCoreTests/DriftAlertTests.swift @@ -1,65 +1,57 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("DriftAlert") -struct DriftAlertTests { +final class DriftAlertTests: XCTestCase { - @Test("No alert with fewer than two poor results") - func notEnough() { + func testNotEnough() { let records = [ record(avg: 4.0, at: 1000) ] - #expect(DriftAlert.compute(from: records) == nil) + XCTAssertNil(DriftAlert.compute(from: records)) } - @Test("Alert on two poor results one hour apart") - func oneHourApart() { + func testOneHourApart() { let records = [ record(avg: 4.0, at: 1000), record(avg: 5.0, at: 4600) ] - #expect(DriftAlert.compute(from: records) != nil) + XCTAssertNotNil(DriftAlert.compute(from: records)) } - @Test("No alert if same day and under one hour") - func sameDayUnderHour() { + func testSameDayUnderHour() { let records = [ record(avg: 4.0, at: 1000), record(avg: 5.0, at: 2000) ] - #expect(DriftAlert.compute(from: records) == nil) + XCTAssertNil(DriftAlert.compute(from: records)) } - @Test("Alert on distinct days") - func distinctDays() { + func testDistinctDays() { let day1 = record(avg: 4.0, at: 0) let day2 = record(avg: 5.0, at: 86400 + 1000) - #expect(DriftAlert.compute(from: [day1, day2]) != nil) + XCTAssertNotNil(DriftAlert.compute(from: [day1, day2])) } - @Test("Non-poor records do not trigger") - func nonPoor() { + func testNonPoor() { let records = [ record(avg: 1.0, at: 0), record(avg: 1.5, at: 86400) ] - #expect(DriftAlert.compute(from: records) == nil) + XCTAssertNil(DriftAlert.compute(from: records)) } - @Test("Non-poor records break the consecutive poor run") - func nonPoorBreaksRun() { + func testNonPoorBreaksRun() { let records = [ record(avg: 4.0, at: 0), // poor record(avg: 4.5, at: 86400), // poor, far apart record(avg: 1.0, at: 90000), // good — breaks the run record(avg: 4.0, at: 92000) // poor, recent but close to previous poor ] - #expect(DriftAlert.compute(from: records) == nil) + XCTAssertNil(DriftAlert.compute(from: records)) } - @Test("Only the final consecutive poor run is considered") - func onlySuffixRun() { + func testOnlySuffixRun() { let records = [ record(avg: 4.0, at: 0), // poor record(avg: 4.5, at: 18000), // poor, > 1h from first @@ -67,26 +59,24 @@ struct DriftAlertTests { record(avg: 4.0, at: 25000), // poor record(avg: 4.5, at: 26000) // poor, < 1h and same day ] - #expect(DriftAlert.compute(from: records) == nil) + XCTAssertNil(DriftAlert.compute(from: records)) } - @Test("Final consecutive poor run alerts when far apart") - func suffixRunAlerts() { + func testSuffixRunAlerts() { let records = [ record(avg: 1.0, at: 0), // good record(avg: 4.0, at: 1000), // poor record(avg: 4.5, at: 4600) // poor, 1h after previous ] - #expect(DriftAlert.compute(from: records) != nil) + XCTAssertNotNil(DriftAlert.compute(from: records)) } - @Test("A single final poor record after good records does not alert") - func singleFinalPoor() { + func testSingleFinalPoor() { let records = [ record(avg: 1.0, at: 0), record(avg: 4.0, at: 86400) ] - #expect(DriftAlert.compute(from: records) == nil) + XCTAssertNil(DriftAlert.compute(from: records)) } private func record(avg: Double, at offset: TimeInterval) -> VerificationRecord { diff --git a/Tests/ICCeryCoreTests/FileHelpersTests.swift b/Tests/ICCeryCoreTests/FileHelpersTests.swift index 0854f8c..32a99a8 100644 --- a/Tests/ICCeryCoreTests/FileHelpersTests.swift +++ b/Tests/ICCeryCoreTests/FileHelpersTests.swift @@ -1,4 +1,4 @@ -import Testing +import XCTest import Foundation @testable import ICCeryCore @@ -13,91 +13,88 @@ private func touch(_ url: URL, _ contents: String = "x") throws { try contents.write(to: url, atomically: true, encoding: .utf8) } -@Suite("PathSecurity") -struct PathSecurityTests { - @Test func rejectsTraversalAndSeparators() { +final class PathSecurityTests: XCTestCase { + func testRejectsTraversalAndSeparators() { for bad in ["a/b", "a\\b", "..", "a/../b", "", "..x"] { - #expect(!PathSecurity.isValidBasename(bad)) - #expect(throws: PathSecurity.Error.self) { - try PathSecurity.sanitizeBasename(bad) + XCTAssertFalse(PathSecurity.isValidBasename(bad)) + XCTAssertThrowsError(try PathSecurity.sanitizeBasename(bad)) { error in + XCTAssertTrue(error is PathSecurity.Error) } } } - @Test func acceptsNormalNames() { + func testAcceptsNormalNames() { for good in ["target", "My Target 01", "écheneau-ümläut", "a.b"] { - #expect(PathSecurity.isValidBasename(good)) + XCTAssertTrue(PathSecurity.isValidBasename(good)) } } - @Test func resolveSafeCwdPrefersExplicit() throws { + func testResolveSafeCwdPrefersExplicit() throws { let dir = try tempDir() - #expect(PathSecurity.resolveSafeCwd(dir) == dir) + XCTAssertEqual(PathSecurity.resolveSafeCwd(dir), dir) } - @Test func resolveSafeCwdNeverReturnsNil() { + func testResolveSafeCwdNeverReturnsNil() { let missing = URL(fileURLWithPath: "/nonexistent-\(UUID().uuidString)") let resolved = PathSecurity.resolveSafeCwd(missing) - #expect(FileManager.default.fileExists(atPath: resolved.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: resolved.path)) } } -@Suite("AtomicFileWriter") -struct AtomicFileWriterTests { - @Test func writesAndLeavesNoTmp() throws { +final class AtomicFileWriterTests: XCTestCase { + func testWritesAndLeavesNoTmp() throws { let dir = try tempDir() let url = dir.appendingPathComponent("state.json") try AtomicFileWriter.write(Data("{\"a\":1}".utf8), to: url) - #expect(try String(contentsOf: url, encoding: .utf8) == "{\"a\":1}") - #expect(!FileManager.default.fileExists(atPath: url.appendingPathExtension("tmp").path)) + XCTAssertEqual(try String(contentsOf: url, encoding: .utf8), "{\"a\":1}") + XCTAssertFalse(FileManager.default.fileExists(atPath: url.appendingPathExtension("tmp").path)) } - @Test func overwritesExistingAtomically() throws { + func testOverwritesExistingAtomically() throws { let dir = try tempDir() let url = dir.appendingPathComponent("f.txt") try AtomicFileWriter.write("one", to: url) try AtomicFileWriter.write("two-longer", to: url) - #expect(try String(contentsOf: url, encoding: .utf8) == "two-longer") + XCTAssertEqual(try String(contentsOf: url, encoding: .utf8), "two-longer") } - @Test func createsParentDirs() throws { + func testCreatesParentDirs() throws { let dir = try tempDir() let url = dir.appendingPathComponent("a/b/c/deep.json") try AtomicFileWriter.write("{}", to: url) - #expect(FileManager.default.fileExists(atPath: url.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: url.path)) } } -@Suite("ArtefactProbe") -struct ArtefactProbeTests { - @Test func verifyProgression() throws { +final class ArtefactProbeTests: XCTestCase { + func testVerifyProgression() throws { let dir = try tempDir() var v = ArtefactProbe.verify(basename: "t", cwd: dir) - #expect(v == StageArtefacts()) + XCTAssertEqual(v, StageArtefacts()) try touch(dir.appendingPathComponent("t.ti1")) v = ArtefactProbe.verify(basename: "t", cwd: dir) - #expect(v.stage1Complete && !v.stage2Complete && !v.stage3Complete) + XCTAssertTrue(v.stage1Complete && !v.stage2Complete && !v.stage3Complete) try touch(dir.appendingPathComponent("t.ti2")) try touch(dir.appendingPathComponent("t.ti3")) v = ArtefactProbe.verify(basename: "t", cwd: dir) - #expect(v.stage2Complete && v.stage3Complete && !v.stage4Complete) + XCTAssertTrue(v.stage2Complete && v.stage3Complete && !v.stage4Complete) try touch(dir.appendingPathComponent("t.icc")) v = ArtefactProbe.verify(basename: "t", cwd: dir) - #expect(v.stage4Complete && v.profilePath?.pathExtension == "icc") + XCTAssertTrue(v.stage4Complete && v.profilePath?.pathExtension == "icc") } - @Test func icmWinsOverIcc() throws { + func testIcmWinsOverIcc() throws { let dir = try tempDir() try touch(dir.appendingPathComponent("p.icc")) try touch(dir.appendingPathComponent("p.icm")) let profile = ArtefactProbe.resolveProfile(basename: "p", cwd: dir) - #expect(profile?.pathExtension == "icm") + XCTAssertEqual(profile?.pathExtension, "icm") } - @Test func enumeratesPassesPagesAndCAL() throws { + func testEnumeratesPassesPagesAndCAL() throws { let dir = try tempDir() for name in [ "t.ti1", "t.ti2", "t.tif", "t.2.tif", "t_03.tif", @@ -115,15 +112,15 @@ struct ArtefactProbeTests { "t.ti3", "t_pass1.ti3", "t_pass2.ti3", "t.icc", "t.gam", "CAL_t.ti1", "CAL_t.cal", ] { - #expect(names.contains(expected), "missing \(expected)") + XCTAssertTrue(names.contains(expected), "missing \(expected)") } - #expect(!names.contains("other.ti1")) - #expect(!names.contains("t.txt")) - #expect(!names.contains("CAL_other.ti1")) + XCTAssertFalse(names.contains("other.ti1")) + XCTAssertFalse(names.contains("t.txt")) + XCTAssertFalse(names.contains("CAL_other.ti1")) } - @Test func emptyDirReturnsEmpty() throws { + func testEmptyDirReturnsEmpty() throws { let dir = try tempDir() - #expect(ArtefactProbe.existingArtefacts(basename: "x", cwd: dir).isEmpty) + XCTAssertTrue(ArtefactProbe.existingArtefacts(basename: "x", cwd: dir).isEmpty) } } diff --git a/Tests/ICCeryCoreTests/GamutGeometryBuilderTests.swift b/Tests/ICCeryCoreTests/GamutGeometryBuilderTests.swift index 5802421..d3f1b31 100644 --- a/Tests/ICCeryCoreTests/GamutGeometryBuilderTests.swift +++ b/Tests/ICCeryCoreTests/GamutGeometryBuilderTests.swift @@ -1,15 +1,13 @@ -import Testing +import XCTest import SceneKit import ICCeryCore @testable import ICCery /// ``GamutSceneGeometryBuilder`` edge-case tests. -@Suite("Gamut scene geometry builder") @MainActor -struct GamutGeometryBuilderTests { +final class GamutGeometryBuilderTests: XCTestCase { - @Test("Drops out-of-bounds faces from the element without crashing") - func dropsOutOfBoundsFaces() { + func testDropsOutOfBoundsFaces() { let white = GamutVertex( lab: LabColor(l: 100, a: 0, b: 0), rgb: DisplayRGB(r: 1, g: 1, b: 1) @@ -28,6 +26,6 @@ struct GamutGeometryBuilderTests { let (_, element) = GamutSceneGeometryBuilder.geometry(for: mesh) - #expect(element.primitiveCount == 1, "Only the in-bounds face should be in the index buffer") + XCTAssertEqual(element.primitiveCount, 1, "Only the in-bounds face should be in the index buffer") } } diff --git a/Tests/ICCeryCoreTests/GamutMeshParserTests.swift b/Tests/ICCeryCoreTests/GamutMeshParserTests.swift index c3643a2..cb54693 100644 --- a/Tests/ICCeryCoreTests/GamutMeshParserTests.swift +++ b/Tests/ICCeryCoreTests/GamutMeshParserTests.swift @@ -1,10 +1,9 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore /// ``GamutMeshParser`` acceptance + edge-case tests. -@Suite("Gamut mesh parser") -struct GamutMeshParserTests { +final class GamutMeshParserTests: XCTestCase { /// Returns the bundled real `sRGB.gam` in `Resources/Argyll/reference_gamuts`. private var bundledSRGBGamURL: URL { @@ -13,16 +12,14 @@ struct GamutMeshParserTests { return resource.appendingPathComponent("Argyll/reference_gamuts/sRGB.gam") } - @Test("Parses bundled sRGB.gam") - func parsesBundledSRGB() throws { + func testParsesBundledSRGB() throws { let mesh = try GamutMeshParser.parse(url: bundledSRGBGamURL) - #expect(mesh.vertices.count == 448, "sRGB.gam has 448 vertices") - #expect(mesh.faces.count == 892, "sRGB.gam has 892 faces") + XCTAssertEqual(mesh.vertices.count, 448, "sRGB.gam has 448 vertices") + XCTAssertEqual(mesh.faces.count, 892, "sRGB.gam has 892 faces") } - @Test("Discards VERTEX_NO and uses push-order indices") - func discardsVertexNo() throws { + func testDiscardsVertexNo() throws { let text = """ GAMUT NUMBER_OF_FIELDS 4 @@ -49,14 +46,13 @@ struct GamutMeshParserTests { let mesh = try GamutMeshParser.parse(text: text) - #expect(mesh.vertices.count == 4) - #expect(mesh.faces.count == 2) - #expect(mesh.vertices[0].lab == LabColor(l: 10, a: 20, b: 30)) - #expect(mesh.vertices[3].lab == LabColor(l: 40, a: 50, b: 60)) + XCTAssertEqual(mesh.vertices.count, 4) + XCTAssertEqual(mesh.faces.count, 2) + XCTAssertEqual(mesh.vertices[0].lab, LabColor(l: 10, a: 20, b: 30)) + XCTAssertEqual(mesh.vertices[3].lab, LabColor(l: 40, a: 50, b: 60)) } - @Test("Ignores comments and blank lines") - func ignoresComments() throws { + func testIgnoresComments() throws { let text = """ # Header comment NUMBER_OF_FIELDS 4 @@ -81,12 +77,11 @@ struct GamutMeshParserTests { """ let mesh = try GamutMeshParser.parse(text: text) - #expect(mesh.vertices.count == 2) - #expect(mesh.faces.count == 1) + XCTAssertEqual(mesh.vertices.count, 2) + XCTAssertEqual(mesh.faces.count, 1) } - @Test("Remaps coordinates to x=a*, y=L*, z=b*") - func remapsCoordinates() throws { + func testRemapsCoordinates() throws { let text = """ NUMBER_OF_FIELDS 4 BEGIN_DATA_FORMAT @@ -99,11 +94,10 @@ struct GamutMeshParserTests { """ let mesh = try GamutMeshParser.parse(text: text) - #expect(mesh.vertices.first?.position == SIMD3(-20, 50, 80)) + XCTAssertEqual(mesh.vertices.first?.position, SIMD3(-20, 50, 80)) } - @Test("Computes per-vertex sRGB colour") - func computesVertexColor() throws { + func testComputesVertexColor() throws { let text = """ NUMBER_OF_FIELDS 4 BEGIN_DATA_FORMAT @@ -116,14 +110,13 @@ struct GamutMeshParserTests { """ let mesh = try GamutMeshParser.parse(text: text) - let white = try #require(mesh.vertices.first).rgb - #expect(white.r > 0.95) - #expect(white.g > 0.95) - #expect(white.b > 0.95) + let white = try XCTUnwrap(mesh.vertices.first).rgb + XCTAssertTrue(white.r > 0.95) + XCTAssertTrue(white.g > 0.95) + XCTAssertTrue(white.b > 0.95) } - @Test("Drops out-of-bounds face indices") - func dropsOutOfBoundsFaces() throws { + func testDropsOutOfBoundsFaces() throws { let text = """ NUMBER_OF_FIELDS 4 BEGIN_DATA_FORMAT @@ -146,21 +139,23 @@ struct GamutMeshParserTests { """ let mesh = try GamutMeshParser.parse(text: text) - #expect(mesh.faces.count == 1) + XCTAssertEqual(mesh.faces.count, 1) } - @Test("Throws on empty file") - func throwsOnEmptyFile() { - #expect(throws: GamutMeshParseError.noDataBlock) { - _ = try GamutMeshParser.parse(text: "") + func testThrowsOnEmptyFile() { + XCTAssertThrowsError(try GamutMeshParser.parse(text: "")) { error in + guard case GamutMeshParseError.noDataBlock = error else { + return XCTFail("Expected GamutMeshParseError.noDataBlock, got \(error)") + } } } - @Test("Throws when file is missing") - func throwsWhenMissing() { + func testThrowsWhenMissing() { let url = URL(fileURLWithPath: "/nonexistent/path/to/mesh.gam") - #expect(throws: GamutMeshParseError.missingFile) { - _ = try GamutMeshParser.parse(url: url) + XCTAssertThrowsError(try GamutMeshParser.parse(url: url)) { error in + guard case GamutMeshParseError.missingFile = error else { + return XCTFail("Expected GamutMeshParseError.missingFile, got \(error)") + } } } } diff --git a/Tests/ICCeryCoreTests/IccgamutArgsTests.swift b/Tests/ICCeryCoreTests/IccgamutArgsTests.swift index 2ef2ba9..86916a9 100644 --- a/Tests/ICCeryCoreTests/IccgamutArgsTests.swift +++ b/Tests/ICCeryCoreTests/IccgamutArgsTests.swift @@ -1,16 +1,14 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("IccgamutArgs") -struct IccgamutArgsTests { +final class IccgamutArgsTests: XCTestCase { - @Test("Density is 10 and not a directory") - func densityNotDirectory() throws { + func testDensityNotDirectory() throws { let config = IccgamutConfig( profileURL: URL(fileURLWithPath: "/tmp/MyProfile.icc") ) let args = try IccgamutArgs.build(config: config) - #expect(args == ["-v", "-d", "10", "/tmp/MyProfile.icc"]) + XCTAssertEqual(args, ["-v", "-d", "10", "/tmp/MyProfile.icc"]) } } diff --git a/Tests/ICCeryCoreTests/LpArgsTests.swift b/Tests/ICCeryCoreTests/LpArgsTests.swift index 4dcb22a..893aa96 100644 --- a/Tests/ICCeryCoreTests/LpArgsTests.swift +++ b/Tests/ICCeryCoreTests/LpArgsTests.swift @@ -1,12 +1,11 @@ -import Testing +import XCTest import Foundation @testable import ICCeryCore /// Issue 15 — `lp` argv goldens (docs/11 `build_lp_args`). /// `-d`/`options`/`-t` handling is in `CupsService`; these tests cover /// flag order, captured-option precedence, and sanitisation. -@Suite("LpArgs") -struct LpArgsTests { +final class LpArgsTests: XCTestCase { private let tiff = "/tmp/work/target_001.tif" private let queue = "EPSON_XP_55_Series" @@ -20,112 +19,100 @@ struct LpArgsTests { options: options, optionKeys: optionKeys) } - @Test("Header: -d queue -t title, both AP_* first, TIFF last") - func header() throws { + func testHeader() throws { let argv = try build() - #expect(Array(argv[0...1]) == ["-d", queue]) - #expect(Array(argv[2...3]) == ["-t", "ICCery Target - target_001.tif"]) - #expect(Array(argv[4...5]) - == ["-o", "AP_ColorMatchingMode=AP_ApplicationColorMatching"]) - #expect(Array(argv[6...7]) - == ["-o", "AP.ColorMatchingMode=AP_ApplicationColorMatching"]) - #expect(argv.last == tiff) - #expect(!argv.contains { $0 == "raw" || $0 == "-o raw" }) + XCTAssertEqual(Array(argv[0...1]), ["-d", queue]) + XCTAssertEqual(Array(argv[2...3]), ["-t", "ICCery Target - target_001.tif"]) + XCTAssertEqual(Array(argv[4...5]), ["-o", "AP_ColorMatchingMode=AP_ApplicationColorMatching"]) + XCTAssertEqual(Array(argv[6...7]), ["-o", "AP.ColorMatchingMode=AP_ApplicationColorMatching"]) + XCTAssertEqual(argv.last, tiff) + XCTAssertFalse(argv.contains { $0 == "raw" || $0 == "-o raw" }) } - @Test("Never emits -o raw; captured raw= is dropped") - func neverRaw() throws { + func testNeverRaw() throws { let argv = try build(options: PrintOptions( cupsOptions: "raw=true MediaType=Photo")) for (i, arg) in argv.enumerated() where arg == "-o" { - #expect(argv[i + 1] != "raw") - #expect(argv[i + 1] != "raw=true") + XCTAssertNotEqual(argv[i + 1], "raw") + XCTAssertNotEqual(argv[i + 1], "raw=true") } - #expect(!argv.contains { $0.hasPrefix("raw=") }) - #expect(argv.contains("MediaType=Photo")) + XCTAssertFalse(argv.contains { $0.hasPrefix("raw=") }) + XCTAssertTrue(argv.contains("MediaType=Photo")) } - @Test("Captured options replayed after AP_* headers") - func capturedReplay() throws { + func testCapturedReplay() throws { let argv = try build(options: PrintOptions( cupsOptions: "InputSlot=Rear MediaType=Photo")) let rear = argv.firstIndex(of: "InputSlot=Rear")! let apFirst = argv.firstIndex(of: "AP_ColorMatchingMode=AP_ApplicationColorMatching")! - #expect(rear > apFirst) + XCTAssertTrue(rear > apFirst) } - @Test("Captured wins: media key present → derived media skipped") - func capturedWinsMedia() throws { + func testCapturedWinsMedia() throws { let argv = try build( options: PrintOptions( mediaType: "Plain", cupsOptions: "MediaType=Glossy"), optionKeys: ["MediaType"]) - #expect(argv.contains("MediaType=Glossy")) - #expect(!argv.contains("MediaType=Plain")) + XCTAssertTrue(argv.contains("MediaType=Glossy")) + XCTAssertFalse(argv.contains("MediaType=Plain")) } - @Test("Media emitted via detected key when not captured") - func mediaDerived() throws { + func testMediaDerived() throws { let argv = try build( options: PrintOptions(mediaType: "SemiGloss"), optionKeys: ["CNIJMediaType", "MediaType"]) // CNIJMediaType wins over MediaType in detection order. - #expect(argv.contains("CNIJMediaType=SemiGloss")) - #expect(!argv.contains("MediaType=SemiGloss")) + XCTAssertTrue(argv.contains("CNIJMediaType=SemiGloss")) + XCTAssertFalse(argv.contains("MediaType=SemiGloss")) } - @Test("Driver bypass emitted when absent, skipped when captured") - func bypassRules() throws { + func testBypassRules() throws { let withBypass = try build( optionKeys: ["EPIJ_CMat"]) - #expect(withBypass.contains("EPIJ_CMat=3")) + XCTAssertTrue(withBypass.contains("EPIJ_CMat=3")) let captured = try build( options: PrintOptions(cupsOptions: "EPIJ_CMat=1"), optionKeys: ["EPIJ_CMat"]) // Captured value kept, detection not re-applied. - #expect(captured.filter { $0.hasPrefix("EPIJ_CMat") } - == ["EPIJ_CMat=1"]) + XCTAssertEqual(captured.filter { $0.hasPrefix("EPIJ_CMat") }, ["EPIJ_CMat=1"]) } - @Test("Orientation: portrait=3 landscape=4; captured wins") - func orientation() throws { - #expect(try build(options: PrintOptions(orientation: "portrait")) + func testOrientation() throws { + XCTAssertTrue(try build(options: PrintOptions(orientation: "portrait")) .contains("orientation-requested=3")) - #expect(try build(options: PrintOptions(orientation: "landscape")) + XCTAssertTrue(try build(options: PrintOptions(orientation: "landscape")) .contains("orientation-requested=4")) let capturedOrients = try build(options: PrintOptions( orientation: "landscape", cupsOptions: "orientation-requested=5")) - #expect(!capturedOrients.contains("orientation-requested=4")) - #expect(capturedOrients.contains("orientation-requested=5")) + XCTAssertFalse(capturedOrients.contains("orientation-requested=4")) + XCTAssertTrue(capturedOrients.contains("orientation-requested=5")) } - @Test("PageSize emitted unless captured") - func pageSize() throws { - #expect(try build(options: PrintOptions(paperSize: "A4")) + func testPageSize() throws { + XCTAssertTrue(try build(options: PrintOptions(paperSize: "A4")) .contains("PageSize=A4")) let capturedSize = try build(options: PrintOptions( paperSize: "A4", cupsOptions: "PageSize=Letter")) - #expect(!capturedSize.contains("PageSize=A4")) - #expect(capturedSize.contains("PageSize=Letter")) + XCTAssertFalse(capturedSize.contains("PageSize=A4")) + XCTAssertTrue(capturedSize.contains("PageSize=Letter")) } - @Test("Sanitise rejects `;`, newline, and shell metachars") - func sanitise() throws { - #expect(throws: LpArgsError.self) { - _ = try build(options: PrintOptions( - cupsOptions: "InputSlot=Rear;rm -rf /")) + func testSanitise() throws { + XCTAssertThrowsError(try build(options: PrintOptions( + cupsOptions: "InputSlot=Rear;rm -rf /"))) { error in + XCTAssertTrue(error is LpArgsError) } - #expect(throws: LpArgsError.self) { - _ = try build(options: PrintOptions( - cupsOptions: "InputSlot=Rear\nMediaType=Photo")) + XCTAssertThrowsError(try build(options: PrintOptions( + cupsOptions: "InputSlot=Rear\nMediaType=Photo"))) { error in + XCTAssertTrue(error is LpArgsError) } - #expect(throws: LpArgsError.self) { - _ = try build(options: PrintOptions( - cupsOptions: "InputSlot=$(whoami)")) + XCTAssertThrowsError(try build(options: PrintOptions( + cupsOptions: "InputSlot=$(whoami)"))) { error in + XCTAssertTrue(error is LpArgsError) } } } diff --git a/Tests/ICCeryCoreTests/MeasurementTests.swift b/Tests/ICCeryCoreTests/MeasurementTests.swift index 3687237..aac8cdb 100644 --- a/Tests/ICCeryCoreTests/MeasurementTests.swift +++ b/Tests/ICCeryCoreTests/MeasurementTests.swift @@ -1,12 +1,10 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("InstrumentParser") -struct InstrumentParserTests { +final class InstrumentParserTests: XCTestCase { - @Test("Parses pretty-printed instlist JSON") - func json() throws { + func testJson() throws { let json = """ { "event": "instruments", @@ -18,134 +16,118 @@ struct InstrumentParserTests { } """ let devices = try InstrumentParser.parse(json) - #expect(devices.count == 3) - #expect(devices[0].port == 1) - #expect(devices[0].name == "X-Rite i1Pro") - #expect(devices[2].port == 3) + XCTAssertEqual(devices.count, 3) + XCTAssertEqual(devices[0].port, 1) + XCTAssertEqual(devices[0].name, "X-Rite i1Pro") + XCTAssertEqual(devices[2].port, 3) } - @Test("Falls back to regex for legacy instlist text") - func regexFallback() throws { + func testRegexFallback() throws { let text = """ 1: 'X-Rite i1Pro' on usb 2: 'ColorMunki Smile' """ + "\n" let devices = try InstrumentParser.parse(text) - #expect(devices.count == 2) - #expect(devices[0].port == 1) - #expect(devices[1].name == "ColorMunki Smile") + XCTAssertEqual(devices.count, 2) + XCTAssertEqual(devices[0].port, 1) + XCTAssertEqual(devices[1].name, "ColorMunki Smile") } - @Test("Empty output returns no devices") - func empty() throws { - #expect(try InstrumentParser.parse("").isEmpty) + func testEmpty() throws { + XCTAssertTrue(try InstrumentParser.parse("").isEmpty) } } -@Suite("ChartreadArgs") -struct ChartreadArgsTests { +final class ChartreadArgsTests: XCTestCase { - @Test("Baseline argv and port 1 omits -c") - func baseline() throws { + func testBaseline() throws { let config = ChartreadConfig(basename: "target", selectedPort: 1) let args = try ChartreadArgs.build(config: config) - #expect(args == ["-v", "-u", "target"]) + XCTAssertEqual(args, ["-v", "-u", "target"]) } - @Test("Port > 1 emits -c") - func portArgument() throws { + func testPortArgument() throws { let config = ChartreadConfig(basename: "target", selectedPort: 3) let args = try ChartreadArgs.build(config: config) - #expect(args == ["-v", "-u", "-c", "3", "target"]) + XCTAssertEqual(args, ["-v", "-u", "-c", "3", "target"]) } - @Test("LEDs emit -Y l") - func leds() throws { + func testLeds() throws { let config = ChartreadConfig( basename: "target", selectedPort: 2, enableLEDs: true ) let args = try ChartreadArgs.build(config: config) - #expect(args.contains("-Y")) - #expect(args.contains("l")) + XCTAssertTrue(args.contains("-Y")) + XCTAssertTrue(args.contains("l")) } - @Test("Auto omits -c") - func autoPort() throws { + func testAutoPort() throws { let config = ChartreadConfig(basename: "target") let args = try ChartreadArgs.build(config: config) - #expect(!args.contains("-c")) + XCTAssertFalse(args.contains("-c")) } } -@Suite("ChartreadClassifier") -struct ChartreadClassifierTests { +final class ChartreadClassifierTests: XCTestCase { - @Test("Calibration prompt") - func calibration() { + func testCalibration() { let r = ChartreadClassifier.classify( line: "Place instrument on calibration tile and hit [Space] to calibrate.", previousState: .idle ) - #expect(r.state == .calibrating) + XCTAssertEqual(r.state, .calibrating) } - @Test("Strip awaiting") - func awaitingStrip() { + func testAwaitingStrip() { let r = ChartreadClassifier.classify( line: "Hit [Space] to read strip A", previousState: .calibrating ) - #expect(r.state == .awaitingStrip) + XCTAssertEqual(r.state, .awaitingStrip) } - @Test("Done prompt") - func done() { + func testDone() { let r = ChartreadClassifier.classify( line: "'d' if/when done", previousState: .awaitingStrip ) - #expect(r.state == .allStripsRead) + XCTAssertEqual(r.state, .allStripsRead) } - @Test("XY place sheet") - func placeSheet() { + func testPlaceSheet() { let r = ChartreadClassifier.classify( line: "Please place sheet 1 of 2 on the table", previousState: .idle ) - #expect(r.state == .tablePlaceSheet) - #expect(r.sheetNumber == 1) - #expect(r.sheetTotal == 2) + XCTAssertEqual(r.state, .tablePlaceSheet) + XCTAssertEqual(r.sheetNumber, 1) + XCTAssertEqual(r.sheetTotal, 2) } - @Test("XY locate patch") - func locatePatch() { + func testLocatePatch() { let r = ChartreadClassifier.classify( line: "locate patch A1 with the sight,", previousState: .tablePlaceSheet ) - #expect(r.state == .tableAlign) - #expect(r.alignmentPatch == "A1") + XCTAssertEqual(r.state, .tableAlign) + XCTAssertEqual(r.alignmentPatch, "A1") } - @Test("Remove sheet notice preserves state") - func removeNotice() { + func testRemoveNotice() { let r = ChartreadClassifier.classify( line: "Please remove last sheet from table", previousState: .tablePlaceSheet ) - #expect(r.state == .tablePlaceSheet) - #expect(r.isRemoveSheetNotice == true) + XCTAssertEqual(r.state, .tablePlaceSheet) + XCTAssertEqual(r.isRemoveSheetNotice, true) } } -@Suite("ChartreadRow") -struct ChartreadRowTests { +final class ChartreadRowTests: XCTestCase { - @Test("Decodes row JSON") - func decode() throws { + func testDecode() throws { let json = """ {"event": "row_complete", "row_id": "A", "row_index": 0, "total_rows": 2, "patch_count": 1, "patches": [ @@ -155,13 +137,12 @@ struct ChartreadRowTests { ]} """ let row = try JSONDecoder().decode(ChartreadRow.self, from: Data(json.utf8)) - #expect(row.rowId == "A") - #expect(row.patchCount == 1) - #expect(row.patches[0].measured.lab?.l == 51) + XCTAssertEqual(row.rowId, "A") + XCTAssertEqual(row.patchCount, 1) + XCTAssertEqual(row.patches[0].measured.lab?.l, 51) } - @Test("Decodes a row carrying both XYZ and Lab arrays") - func decodeXYZAndLab() throws { + func testDecodeXYZAndLab() throws { let json = """ {"event": "row_complete", "row_id": "B", "row_index": 1, "total_rows": 2, "patch_count": 1, "patches": [ @@ -171,88 +152,78 @@ struct ChartreadRowTests { """ let row = try JSONDecoder().decode(ChartreadRow.self, from: Data(json.utf8)) let measured = row.patches[0].measured - #expect(measured.xyz == CIEXYZ(x: 30.5, y: 32.1, z: 25.9)) - #expect(measured.lab == CIELab(l: 63.4, a: 2.5, b: -8.2)) + XCTAssertEqual(measured.xyz, CIEXYZ(x: 30.5, y: 32.1, z: 25.9)) + XCTAssertEqual(measured.lab, CIELab(l: 63.4, a: 2.5, b: -8.2)) } - @Test("XYZColor/CIEXYZ encode as an unkeyed three-number array") - func xyzWireEncoding() throws { + func testXyzWireEncoding() throws { for color in [XYZColor(x: 1.5, y: 2.5, z: 3.5), CIEXYZ(x: 1.5, y: 2.5, z: 3.5)] { let value = try JSONSerialization.jsonObject( with: JSONEncoder().encode(color)) - #expect(value as? [Double] == [1.5, 2.5, 3.5]) + XCTAssertEqual(value as? [Double], [1.5, 2.5, 3.5]) } } - @Test("LabColor/CIELab encode as an unkeyed three-number array") - func labWireEncoding() throws { + func testLabWireEncoding() throws { for color in [LabColor(l: 50, a: -1, b: 2), CIELab(l: 50, a: -1, b: 2)] { let value = try JSONSerialization.jsonObject( with: JSONEncoder().encode(color)) - #expect(value as? [Double] == [50, -1, 2]) + XCTAssertEqual(value as? [Double], [50, -1, 2]) } } - @Test("PatchColor keeps the XYZ and Lab keys over unkeyed arrays") - func patchColorKeys() throws { + func testPatchColorKeys() throws { let color = PatchColor( xyz: CIEXYZ(x: 10, y: 20, z: 30), lab: CIELab(l: 55, a: 1, b: -2)) let object = try JSONSerialization.jsonObject( with: JSONEncoder().encode(color)) as? [String: Any] - #expect(object?["XYZ"] as? [Double] == [10, 20, 30]) - #expect(object?["Lab"] as? [Double] == [55, 1, -2]) - #expect(object?["spectral"] == nil) + XCTAssertEqual(object?["XYZ"] as? [Double], [10, 20, 30]) + XCTAssertEqual(object?["Lab"] as? [Double], [55, 1, -2]) + XCTAssertNil(object?["spectral"]) } } -@Suite("ColourMath") -struct ColourMathTests { +final class ColourMathTests: XCTestCase { - @Test("White XYZ to Lab") - func whiteLab() { + func testWhiteLab() { let white = XYZColor(x: 96.4212, y: 100.0, z: 82.5188) let lab = LabColorMath.xyzToLab(white) - #expect(abs(lab.l - 100) < 0.5) - #expect(abs(lab.a) < 0.5) - #expect(abs(lab.b) < 0.5) + XCTAssertTrue(abs(lab.l - 100) < 0.5) + XCTAssertTrue(abs(lab.a) < 0.5) + XCTAssertTrue(abs(lab.b) < 0.5) } - @Test("Lab to sRGB roundtrip is clamped") - func labToSRGB() { + func testLabToSRGB() { let red = LabColor(l: 55, a: 80, b: 70) let rgb = LabColorMath.labToSRGB(red) - #expect(rgb.r > 0.8) - #expect(rgb.g < 0.2) - #expect(rgb.b < 0.2) + XCTAssertTrue(rgb.r > 0.8) + XCTAssertTrue(rgb.g < 0.2) + XCTAssertTrue(rgb.b < 0.2) } - @Test("Pad white returns DisplayRGB") - func padWhite() { + func testPadWhite() { let white = LabColor(l: 95, a: 0, b: 0) let rgb = LabColorMath.labToSRGB(white) - #expect(rgb.r > 0.9) - #expect(rgb.g > 0.9) - #expect(rgb.b > 0.9) + XCTAssertTrue(rgb.r > 0.9) + XCTAssertTrue(rgb.g > 0.9) + XCTAssertTrue(rgb.b > 0.9) } - @Test("Standard CIEDE2000 vector (Sharma)") - func ciede2000() { + func testCiede2000() { let a = LabColor(l: 50, a: -1.3802, b: -84.2814) let b = LabColor(l: 50, a: 0.0000, b: -82.7485) - #expect(abs(ColorDifference.deltaE00(a, b) - 1.00) < 0.001) + XCTAssertTrue(abs(ColorDifference.deltaE00(a, b) - 1.00) < 0.001) } - @Test("Classification respects thresholds") - func classify() { - #expect(ColorDifference.classify(deltaE: 0.5, goodMax: 2.0, warningMax: 5.0) == .good) - #expect(ColorDifference.classify(deltaE: 3.0, goodMax: 2.0, warningMax: 5.0) == .warning) - #expect(ColorDifference.classify(deltaE: 6.0, goodMax: 2.0, warningMax: 5.0) == .bad) + func testClassify() { + XCTAssertEqual(ColorDifference.classify(deltaE: 0.5, goodMax: 2.0, warningMax: 5.0), .good) + XCTAssertEqual(ColorDifference.classify(deltaE: 3.0, goodMax: 2.0, warningMax: 5.0), .warning) + XCTAssertEqual(ColorDifference.classify(deltaE: 6.0, goodMax: 2.0, warningMax: 5.0), .bad) } } -@Suite("MeasurementArtefacts") -struct MeasurementArtefactTests { +final class MeasurementArtefactTests: XCTestCase { private func makeCwd() throws -> URL { let url = FileManager.default.temporaryDirectory @@ -261,8 +232,7 @@ struct MeasurementArtefactTests { return url } - @Test("Discovers passes in order") - func discovery() throws { + func testDiscovery() throws { let cwd = try makeCwd() defer { try? FileManager.default.removeItem(at: cwd) } @@ -271,11 +241,10 @@ struct MeasurementArtefactTests { try "C".write(to: cwd.appendingPathComponent("target_pass10.ti3"), atomically: true, encoding: .utf8) let passes = MeasurementArtefacts.passSnapshots(basename: "target", cwd: cwd) - #expect(passes.map(\.lastPathComponent) == ["target_pass1.ti3", "target_pass3.ti3", "target_pass10.ti3"]) + XCTAssertEqual(passes.map(\.lastPathComponent), ["target_pass1.ti3", "target_pass3.ti3", "target_pass10.ti3"]) } - @Test("Snapshot and promote are atomic") - func snapshotPromote() throws { + func testSnapshotPromote() throws { let cwd = try makeCwd() defer { try? FileManager.default.removeItem(at: cwd) } @@ -283,16 +252,15 @@ struct MeasurementArtefactTests { try "canonical".write(to: canonical, atomically: true, encoding: .utf8) let pass = try MeasurementArtefacts.snapshotPass(basename: "target", cwd: cwd) - #expect(pass.lastPathComponent == "target_pass1.ti3") - #expect(!FileManager.default.fileExists(atPath: canonical.path)) + XCTAssertEqual(pass.lastPathComponent, "target_pass1.ti3") + XCTAssertFalse(FileManager.default.fileExists(atPath: canonical.path)) let promoted = try MeasurementArtefacts.promotePass(pass: pass, basename: "target", cwd: cwd) - #expect(promoted.lastPathComponent == "target.ti3") - #expect(FileManager.default.fileExists(atPath: promoted.path)) + XCTAssertEqual(promoted.lastPathComponent, "target.ti3") + XCTAssertTrue(FileManager.default.fileExists(atPath: promoted.path)) } - @Test("Pass collisions handled") - func collision() throws { + func testCollision() throws { let cwd = try makeCwd() defer { try? FileManager.default.removeItem(at: cwd) } @@ -302,28 +270,25 @@ struct MeasurementArtefactTests { try "v2".write(to: canonical, atomically: true, encoding: .utf8) let pass2 = try MeasurementArtefacts.snapshotPass(basename: "target", cwd: cwd) - #expect(pass2.lastPathComponent == "target_pass2.ti3") + XCTAssertEqual(pass2.lastPathComponent, "target_pass2.ti3") } } -@Suite("AverageArgs") -struct AverageArgsTests { +final class AverageArgsTests: XCTestCase { - @Test("Requires at least two pass files") - func passCount() { + func testPassCount() { let cwd = URL(fileURLWithPath: "/tmp") let config = AverageConfig( workingDirectory: cwd, basename: "target", passFiles: [URL(fileURLWithPath: "target_pass1.ti3")] ) - #expect(throws: AverageArgError.self) { - _ = try AverageArgs.build(config: config) + XCTAssertThrowsError(try AverageArgs.build(config: config)) { error in + XCTAssertTrue(error is AverageArgError) } } - @Test("Output is last and inputs are relative") - func ordering() throws { + func testOrdering() throws { let cwd = URL(fileURLWithPath: "/tmp") let config = AverageConfig( workingDirectory: cwd, @@ -334,8 +299,8 @@ struct AverageArgsTests { ] ) let args = try AverageArgs.build(config: config) - #expect(args.first == "-v") - #expect(args.last == "target.ti3") - #expect(args == ["-v", "target_pass1.ti3", "target_pass2.ti3", "target.ti3"]) + XCTAssertEqual(args.first, "-v") + XCTAssertEqual(args.last, "target.ti3") + XCTAssertEqual(args, ["-v", "target_pass1.ti3", "target_pass2.ti3", "target.ti3"]) } } diff --git a/Tests/ICCeryCoreTests/PrintcalArgsTests.swift b/Tests/ICCeryCoreTests/PrintcalArgsTests.swift index 57caf9e..cd56634 100644 --- a/Tests/ICCeryCoreTests/PrintcalArgsTests.swift +++ b/Tests/ICCeryCoreTests/PrintcalArgsTests.swift @@ -1,24 +1,21 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("PrintcalArgs") -struct PrintcalArgsTests { +final class PrintcalArgsTests: XCTestCase { private let tmp = URL(fileURLWithPath: "/tmp/out.cal") - @Test("Default printcal argv") - func defaults() throws { + func testDefaults() throws { let config = PrintcalConfig( ti3Basename: "CAL_demo", outputURL: tmp ) let args = try PrintcalArgs.build(config: config) - #expect(args == ["-v", "-e", "-o", "/tmp/out.cal", "CAL_demo"]) + XCTAssertEqual(args, ["-v", "-e", "-o", "/tmp/out.cal", "CAL_demo"]) } - @Test("All options and channel limits") - func allOptions() throws { + func testAllOptions() throws { let config = PrintcalConfig( ti3Basename: "demo", outputURL: tmp, @@ -32,7 +29,7 @@ struct PrintcalArgsTests { ] ) let args = try PrintcalArgs.build(config: config) - #expect(args == [ + XCTAssertEqual(args, [ "-v", "-e", "-I", "-z", "-a", "/tmp/old.cal", @@ -44,39 +41,34 @@ struct PrintcalArgsTests { ]) } - @Test("Whitespace-only previous calibration path emits no -a") - func whitespacePreviousCal() throws { + func testWhitespacePreviousCal() throws { let config = PrintcalConfig( ti3Basename: "demo", outputURL: tmp, previousCalPath: " \n\t " ) let args = try PrintcalArgs.build(config: config) - #expect(!args.contains("-a")) - #expect(args == ["-v", "-e", "-o", "/tmp/out.cal", "CAL_demo"]) + XCTAssertFalse(args.contains("-a")) + XCTAssertEqual(args, ["-v", "-e", "-o", "/tmp/out.cal", "CAL_demo"]) } - @Test("Previous calibration path is trimmed before emission") - func previousCalTrimmed() throws { + func testPreviousCalTrimmed() throws { let config = PrintcalConfig( ti3Basename: "demo", outputURL: tmp, previousCalPath: " /tmp/old.cal " ) let args = try PrintcalArgs.build(config: config) - #expect(args[args.firstIndex(of: "-a")! + 1] == "/tmp/old.cal") + XCTAssertEqual(args[args.firstIndex(of: "-a")! + 1], "/tmp/old.cal") } - @Test("Rejects invalid per-channel limit") - func rejectsBadChannelLimit() { + func testRejectsBadChannelLimit() { let config = PrintcalConfig( ti3Basename: "demo", outputURL: tmp, channelLimits: [PrintcalChannelLimit(channel: "K", percent: 150)] ) - #expect(throws: (any Error).self) { - _ = try PrintcalArgs.build(config: config) - } + XCTAssertThrowsError(try PrintcalArgs.build(config: config)) } } diff --git a/Tests/ICCeryCoreTests/PrinttargTests.swift b/Tests/ICCeryCoreTests/PrinttargTests.swift index bab9fdc..b147afb 100644 --- a/Tests/ICCeryCoreTests/PrinttargTests.swift +++ b/Tests/ICCeryCoreTests/PrinttargTests.swift @@ -1,9 +1,9 @@ import Testing +import XCTest import Foundation @testable import ICCeryCore -@Suite("PrinttargArgs") -struct PrinttargArgsTests { +final class PrinttargArgsTests: XCTestCase { private func config( instrument: PrintInstrument = .i1, @@ -28,136 +28,121 @@ struct PrinttargArgsTests { ) } - @Test("Baseline: -v -u -i i1 -p A4 -R 1 -t 300") - func baseline() throws { + func testBaseline() throws { let args = try PrinttargArgs.build(config: config()) - #expect(args == ["-v", "-u", "-i", "i1", "-p", "A4", + XCTAssertEqual(args, ["-v", "-u", "-i", "i1", "-p", "A4", "-R", "1", "-t", "300", "target"]) } - @Test("Default layout is deterministic -R 1, never bare") - func deterministicDefault() throws { + func testDeterministicDefault() throws { let args = try PrinttargArgs.build(config: config()) - #expect(args.contains("-R")) - #expect(!args.contains("-r")) - #expect(args[args.firstIndex(of: "-R")! + 1] == "1") + XCTAssertTrue(args.contains("-R")) + XCTAssertFalse(args.contains("-r")) + XCTAssertEqual(args[args.firstIndex(of: "-R")! + 1], "1") } - @Test("Custom seed -R N; seed < 1 throws") - func customSeed() throws { + func testCustomSeed() throws { let args = try PrinttargArgs.build(config: config(layout: .customSeed, seed: 42)) - #expect(args[args.firstIndex(of: "-R")! + 1] == "42") - #expect(throws: PrinttargArgError.self) { - try PrinttargArgs.build(config: config(layout: .customSeed, seed: 0)) + XCTAssertEqual(args[args.firstIndex(of: "-R")! + 1], "42") + XCTAssertThrowsError(try PrinttargArgs.build(config: config(layout: .customSeed, seed: 0))) { error in + XCTAssertTrue(error is PrinttargArgError) } } - @Test("Raster emits -r and supersedes seed (printtarg -r, not targen -r)") - func raster() throws { + func testRaster() throws { let args = try PrinttargArgs.build(config: config(layout: .raster, seed: 9)) - #expect(args.contains("-r")) - #expect(!args.contains("-R")) + XCTAssertTrue(args.contains("-r")) + XCTAssertFalse(args.contains("-R")) } - @Test("Label: -d emits the resolved string, not a colour space") - func label() throws { + func testLabel() throws { let args = try PrinttargArgs.build( config: config(label: "ICCery - t - P - I - D - A - 01/02/2026 03:04")) let i = args.firstIndex(of: "-d")! - #expect(args[i + 1].hasPrefix("ICCery - t")) + XCTAssertTrue(args[i + 1].hasPrefix("ICCery - t")) } - @Test("Bit depth: -t 8-bit, -T 16-bit; DPI range 72-600") - func bitDepthAndDPI() throws { - #expect(try PrinttargArgs.build(config: config(bitDepth: .sixteen, dpi: 600)) + func testBitDepthAndDPI() throws { + XCTAssertTrue(try PrinttargArgs.build(config: config(bitDepth: .sixteen, dpi: 600)) .contains("-T")) - #expect(try PrinttargArgs.build(config: config(bitDepth: .eight, dpi: 72)) + XCTAssertTrue(try PrinttargArgs.build(config: config(bitDepth: .eight, dpi: 72)) .contains("-t")) - #expect(throws: PrinttargArgError.self) { - try PrinttargArgs.build(config: config(dpi: 71)) + XCTAssertThrowsError(try PrinttargArgs.build(config: config(dpi: 71))) { error in + XCTAssertTrue(error is PrinttargArgError) } - #expect(throws: PrinttargArgError.self) { - try PrinttargArgs.build(config: config(dpi: 601)) + XCTAssertThrowsError(try PrinttargArgs.build(config: config(dpi: 601))) { error in + XCTAssertTrue(error is PrinttargArgError) } } - @Test("All instruments emit their Argyll code") - func instruments() throws { + func testInstruments() throws { let expected: [(PrintInstrument, String)] = [ (.i1, "i1"), (.p3, "p3"), (.cm, "CM"), (.ss, "SS"), (.dtp20, "20"), (.dtp22, "22"), (.dtp41, "41"), (.dtp51, "51"), ] for (inst, code) in expected { let args = try PrinttargArgs.build(config: config(instrument: inst)) - #expect(args[args.firstIndex(of: "-i")! + 1] == code) + XCTAssertEqual(args[args.firstIndex(of: "-i")! + 1], code) } } - @Test("All fixed page sizes; custom emits WxH in mm") - func pageSizes() throws { + func testPageSizes() throws { for size in PageSize.allCases where size != .custom { let args = try PrinttargArgs.build(config: config(pageSize: size)) - #expect(args[args.firstIndex(of: "-p")! + 1] == size.rawValue) + XCTAssertEqual(args[args.firstIndex(of: "-p")! + 1], size.rawValue) } let custom = try PrinttargArgs.build(config: config( pageSize: .custom, customW: 150, customH: 220)) - #expect(custom[custom.firstIndex(of: "-p")! + 1] == "150x220") + XCTAssertEqual(custom[custom.firstIndex(of: "-p")! + 1], "150x220") } - @Test("Custom page below 50 mm throws") - func customPageTooSmall() { - #expect(throws: PrinttargArgError.self) { - try PrinttargArgs.build(config: config(pageSize: .custom, customW: 49.9)) + func testCustomPageTooSmall() { + XCTAssertThrowsError(try PrinttargArgs.build(config: config(pageSize: .custom, customW: 49.9))) { error in + XCTAssertTrue(error is PrinttargArgError) } - #expect(throws: PrinttargArgError.self) { - try PrinttargArgs.build(config: config(pageSize: .custom, customH: 10)) + XCTAssertThrowsError(try PrinttargArgs.build(config: config(pageSize: .custom, customH: 10))) { error in + XCTAssertTrue(error is PrinttargArgError) } } - @Test("Calibration: -K applies, -I embeds") - func calibrationFlags() throws { + func testCalibrationFlags() throws { let k = try PrinttargArgs.build(config: config(calFile: "/tmp/a.cal")) - #expect(k[k.firstIndex(of: "-K")! + 1] == "/tmp/a.cal") + XCTAssertEqual(k[k.firstIndex(of: "-K")! + 1], "/tmp/a.cal") let i = try PrinttargArgs.build(config: config(calFile: "/tmp/a.cal", calEmbed: true)) - #expect(i[i.firstIndex(of: "-I")! + 1] == "/tmp/a.cal") - #expect(!i.contains("-K")) + XCTAssertEqual(i[i.firstIndex(of: "-I")! + 1], "/tmp/a.cal") + XCTAssertFalse(i.contains("-K")) } - @Test("CAL_ basename never gets -K or -I") - func calProtection() throws { + func testCalProtection() throws { let args = try PrinttargArgs.build( config: config(calFile: "/tmp/a.cal", basename: "CAL_test")) - #expect(!args.contains("-K")) - #expect(!args.contains("-I")) + XCTAssertFalse(args.contains("-K")) + XCTAssertFalse(args.contains("-I")) } - @Test("Whitespace-only label emits no -d; whitespace-only calibration emits no -K/-I") - func whitespaceOptions() throws { + func testWhitespaceOptions() throws { let args = try PrinttargArgs.build( config: config(label: " \n ", calFile: " \t ")) - #expect(!args.contains("-d")) - #expect(!args.contains("-K")) - #expect(!args.contains("-I")) + XCTAssertFalse(args.contains("-d")) + XCTAssertFalse(args.contains("-K")) + XCTAssertFalse(args.contains("-I")) } - @Test("Label and calibration values are trimmed before emission") - func trimmedOptions() throws { + func testTrimmedOptions() throws { let args = try PrinttargArgs.build( config: config(label: " My Label ", calFile: " /tmp/a.cal ")) - #expect(args[args.firstIndex(of: "-d")! + 1] == "My Label") - #expect(args[args.firstIndex(of: "-K")! + 1] == "/tmp/a.cal") + XCTAssertEqual(args[args.firstIndex(of: "-d")! + 1], "My Label") + XCTAssertEqual(args[args.firstIndex(of: "-K")! + 1], "/tmp/a.cal") } - @Test("Unsafe basename throws") - func unsafeBasename() { - #expect(throws: PathSecurity.Error.self) { - try PrinttargArgs.build(config: config(basename: "../x")) + func testUnsafeBasename() { + XCTAssertThrowsError(try PrinttargArgs.build(config: config(basename: "../x"))) { error in + XCTAssertTrue(error is PathSecurity.Error) } } } -@Suite("PrinttargLabel") -struct PrinttargLabelTests { +final class PrinttargLabelTests: XCTestCase { private var fixedDate: Date { var comps = DateComponents() @@ -166,37 +151,33 @@ struct PrinttargLabelTests { return Calendar(identifier: .gregorian).date(from: comps)! } - @Test("Automatic label: ICCery - basename - P - I - DP - AP - DD/MM/YYYY HH:MM") - func automatic() { + func testAutomatic() { let label = PrinttargLabel.automatic( basename: "tgt", metadata: TargetLabelMetadata( printer: "Epson", inkSet: "CMYK", driverPaper: "Photo", actualPaper: "Matte"), date: fixedDate, timeZone: .current) - #expect(label.hasPrefix("ICCery - tgt - Epson - CMYK - Photo - Matte - ")) - #expect(label.hasSuffix("03/02/2026") || label.contains("/02/2026")) + XCTAssertTrue(label.hasPrefix("ICCery - tgt - Epson - CMYK - Photo - Matte - ")) + XCTAssertTrue(label.hasSuffix("03/02/2026") || label.contains("/02/2026")) } - @Test("Missing metadata becomes Unspecified") - func unspecified() { + func testUnspecified() { let label = PrinttargLabel.automatic( basename: "tgt", metadata: TargetLabelMetadata(), date: fixedDate, timeZone: .current) - #expect(label.contains(" - Unspecified - Unspecified - Unspecified - Unspecified - ")) + XCTAssertTrue(label.contains(" - Unspecified - Unspecified - Unspecified - Unspecified - ")) } - @Test("Manual label wins over automatic") - func manualWins() { + func testManualWins() { let resolved = PrinttargLabel.resolved( customLabel: " My Label ", basename: "tgt", metadata: TargetLabelMetadata(), date: fixedDate) - #expect(resolved == "My Label") + XCTAssertEqual(resolved, "My Label") } } -@Suite("PrinttargManifest") -struct PrinttargManifestTests { +final class PrinttargManifestTests: XCTestCase { private let prettySingle = """ Some log line @@ -225,59 +206,52 @@ struct PrinttargManifestTests { } """ - @Test("Decodes a single-page pretty manifest amid log noise") - func singlePage() throws { + func testSinglePage() throws { let m = try PrinttargManifestExtractor.manifest(from: prettySingle) - #expect(m.event == "manifest") - #expect(m.pages.count == 1) - #expect(m.pages[0].filename == "target.tif") - #expect(m.pages[0].patches == 800) + XCTAssertEqual(m.event, "manifest") + XCTAssertEqual(m.pages.count, 1) + XCTAssertEqual(m.pages[0].filename, "target.tif") + XCTAssertEqual(m.pages[0].patches, 800) } - @Test("Multi-page manifest preserves order") - func multiPage() throws { + func testMultiPage() throws { let m = try PrinttargManifestExtractor.manifest(from: prettyMulti) - #expect(m.pages.map(\.filename) == ["p1.tif", "p2.tif"]) + XCTAssertEqual(m.pages.map(\.filename), ["p1.tif", "p2.tif"]) } - @Test("No JSON document → noJSONDocument") - func noJSON() { - #expect(throws: ManifestError.self) { - try PrinttargManifestExtractor.manifest(from: "plain text\nno json") + func testNoJSON() { + XCTAssertThrowsError(try PrinttargManifestExtractor.manifest(from: "plain text\nno json")) { error in + XCTAssertTrue(error is ManifestError) } } - @Test("Wrong event → wrongEvent") - func wrongEvent() { + func testWrongEvent() { let stdout = "{\n \"event\": \"row\",\n \"row\": 1\n}\n" - #expect(throws: ManifestError.self) { - try PrinttargManifestExtractor.manifest(from: stdout) + XCTAssertThrowsError(try PrinttargManifestExtractor.manifest(from: stdout)) { error in + XCTAssertTrue(error is ManifestError) } } - @Test("ROW_COLORS_JSON line is never treated as the manifest") - func rowColorsNotManifest() { + func testRowColorsNotManifest() { let stdout = "ROW_COLORS_JSON: {\"a\":1}\n{\"event\":\"manifest\",\"pages\":[]}" // Extraction only starts at a '{' that begins a trimmed line, // so the ROW_COLORS_JSON line is skipped entirely. let m = try? PrinttargManifestExtractor.manifest(from: stdout) - #expect(m != nil) - #expect(m?.event == "manifest") + XCTAssertNotNil(m) + XCTAssertEqual(m?.event, "manifest") } - @Test("Braces inside a quoted filename do not corrupt the scan") - func bracesInFilename() throws { + func testBracesInFilename() throws { let stdout = "log\n{\n\"event\": \"manifest\",\n\"pages\": [{\"filename\": \"a}b.tif\", \"patches\": 1, \"width_mm\": 50, \"height_mm\": 50}]\n}\n" let m = try PrinttargManifestExtractor.manifest(from: stdout) - #expect(m.pages[0].filename == "a}b.tif") + XCTAssertEqual(m.pages[0].filename, "a}b.tif") } - @Test("Unsafe / non-TIFF filenames rejected") - func unsafeFilenames() { + func testUnsafeFilenames() { for bad in ["../x.tif", "/abs/x.tif", "dir/x.tif", "x.txt", ""] { let stdout = "{\n\"event\":\"manifest\",\"pages\":[{\"filename\":\"\(bad)\",\"patches\":1,\"width_mm\":50,\"height_mm\":50}]\n}" - #expect(throws: ManifestError.self) { - try PrinttargManifestExtractor.manifest(from: stdout) + XCTAssertThrowsError(try PrinttargManifestExtractor.manifest(from: stdout)) { error in + XCTAssertTrue(error is ManifestError) } } } diff --git a/Tests/ICCeryCoreTests/ProcessManagerTests.swift b/Tests/ICCeryCoreTests/ProcessManagerTests.swift index 716bf92..d3a3f3a 100644 --- a/Tests/ICCeryCoreTests/ProcessManagerTests.swift +++ b/Tests/ICCeryCoreTests/ProcessManagerTests.swift @@ -1,4 +1,5 @@ import Testing +import XCTest import Foundation @testable import ICCeryCore @@ -408,65 +409,62 @@ struct ProcessManagerTests { } } -@Suite("ProcessLineDecoder") -struct ProcessLineDecoderTests { - @Test func splitsAcrossChunkBoundaries() { +final class ProcessLineDecoderTests: XCTestCase { + func testSplitsAcrossChunkBoundaries() { var d = ProcessLineDecoder() - #expect(d.feed(Data("he".utf8)) == []) - #expect(d.feed(Data("llo\nwor".utf8)) == ["hello"]) - #expect(d.feed(Data("ld\n".utf8)) == ["world"]) - #expect(d.finish() == nil) + XCTAssertEqual(d.feed(Data("he".utf8)), []) + XCTAssertEqual(d.feed(Data("llo\nwor".utf8)), ["hello"]) + XCTAssertEqual(d.feed(Data("ld\n".utf8)), ["world"]) + XCTAssertNil(d.finish()) } - @Test func crlfIsStripped() { + func testCrlfIsStripped() { var d = ProcessLineDecoder() - #expect(d.feed(Data("a\r\nb\r\n".utf8)) == ["a", "b"]) + XCTAssertEqual(d.feed(Data("a\r\nb\r\n".utf8)), ["a", "b"]) } - @Test func finishReturnsRemainder() { + func testFinishReturnsRemainder() { var d = ProcessLineDecoder() _ = d.feed(Data("x".utf8)) - #expect(d.finish() == "x") - #expect(d.finish() == nil) + XCTAssertEqual(d.finish(), "x") + XCTAssertNil(d.finish()) } } -@Suite("JSONAccumulator") -struct JSONAccumulatorTests { - @Test func multilinePrettyJSON() { +final class JSONAccumulatorTests: XCTestCase { + func testMultilinePrettyJSON() { var acc = JSONAccumulator() - #expect(acc.feed(line: "{") == nil) - #expect(acc.feed(line: " \"k\": 1") == nil) + XCTAssertNil(acc.feed(line: "{")) + XCTAssertNil(acc.feed(line: " \"k\": 1")) let done = acc.feed(line: "}") - #expect(done != nil) + XCTAssertNotNil(done) let obj = try? JSONSerialization.jsonObject(with: done!) as? [String: Int] - #expect(obj?["k"] == 1) + XCTAssertEqual(obj?["k"], 1) } - @Test func nonJSONLinesIgnored() { + func testNonJSONLinesIgnored() { var acc = JSONAccumulator() - #expect(acc.feed(line: "Reading instrument...") == nil) - #expect(acc.feed(line: "still text") == nil) - #expect(acc.completeData == nil) + XCTAssertNil(acc.feed(line: "Reading instrument...")) + XCTAssertNil(acc.feed(line: "still text")) + XCTAssertNil(acc.completeData) } - @Test func decodeTyped() { + func testDecodeTyped() { struct Doc: Decodable { let n: Int } var acc = JSONAccumulator() // Split so the doc completes on the second feed. - #expect(acc.feed(line: "{\"n\":") == nil) + XCTAssertNil(acc.feed(line: "{\"n\":")) let data = acc.feed(line: "7}") - #expect(data != nil) + XCTAssertNotNil(data) let doc = data.flatMap { try? JSONDecoder().decode(Doc.self, from: $0) } - #expect(doc?.n == 7) - #expect(acc.isEmpty) + XCTAssertEqual(doc?.n, 7) + XCTAssertTrue(acc.isEmpty) } } -@Suite("LogSanitizer") -struct LogSanitizerTests { - @Test func homeIsRewritten() { +final class LogSanitizerTests: XCTestCase { + func testHomeIsRewritten() { let path = "\(NSHomeDirectory())/Documents/foo.ti1" - #expect(LogSanitizer.sanitize(path) == "~/Documents/foo.ti1") + XCTAssertEqual(LogSanitizer.sanitize(path), "~/Documents/foo.ti1") } } diff --git a/Tests/ICCeryCoreTests/ProfcheckArgsTests.swift b/Tests/ICCeryCoreTests/ProfcheckArgsTests.swift index 4ac887d..09b461f 100644 --- a/Tests/ICCeryCoreTests/ProfcheckArgsTests.swift +++ b/Tests/ICCeryCoreTests/ProfcheckArgsTests.swift @@ -1,17 +1,15 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("ProfcheckArgs") -struct ProfcheckArgsTests { +final class ProfcheckArgsTests: XCTestCase { - @Test("Hard-coded argv") - func argv() throws { + func testArgv() throws { let config = ProfcheckConfig( ti3URL: URL(fileURLWithPath: "/tmp/target.ti3"), iccURL: URL(fileURLWithPath: "/tmp/target.icc") ) let args = try ProfcheckArgs.build(config: config) - #expect(args == ["-v", "-k", "-s", "-u", "/tmp/target.ti3", "/tmp/target.icc"]) + XCTAssertEqual(args, ["-v", "-k", "-s", "-u", "/tmp/target.ti3", "/tmp/target.icc"]) } } diff --git a/Tests/ICCeryCoreTests/ProfcheckParserTests.swift b/Tests/ICCeryCoreTests/ProfcheckParserTests.swift index 30ea74f..50b165d 100644 --- a/Tests/ICCeryCoreTests/ProfcheckParserTests.swift +++ b/Tests/ICCeryCoreTests/ProfcheckParserTests.swift @@ -1,43 +1,39 @@ import Foundation -import Testing +import XCTest @testable import ICCeryCore -@Suite("ProfcheckParser") -struct ProfcheckParserTests { +final class ProfcheckParserTests: XCTestCase { - @Test("Prefers JSON report with de2000 keys") - func jsonReport() { + func testJsonReport() { let output = """ No of test patches = 52 {"event": "report", "peak_de2000": 2.41, "avg_de2000": 0.85, "rms": 1.02} Profile check complete, errors(CIEDE2000): max. = 9.99, avg. = 9.99, RMS = 9.99 """ let report = ProfcheckParser.parse(output) - #expect(report.isValid == true) - #expect(report.patchCount == 52) - #expect(report.avgDE == 0.85) - #expect(report.maxDE == 2.41) - #expect(report.rmsDE == 1.02) - #expect(report.status == .excellent) + XCTAssertEqual(report.isValid, true) + XCTAssertEqual(report.patchCount, 52) + XCTAssertEqual(report.avgDE, 0.85) + XCTAssertEqual(report.maxDE, 2.41) + XCTAssertEqual(report.rmsDE, 1.02) + XCTAssertEqual(report.status, .excellent) } - @Test("Falls back to legacy text") - func legacyText() { + func testLegacyText() { let output = """ No of test patches = 120 Profile check complete, errors(CIEDE2000): max. = 3.50, avg. = 1.80, RMS = 0.95 """ let report = ProfcheckParser.parse(output) - #expect(report.isValid == true) - #expect(report.patchCount == 120) - #expect(report.avgDE == 1.80) - #expect(report.maxDE == 3.50) - #expect(report.rmsDE == 0.95) - #expect(report.status == .good) + XCTAssertEqual(report.isValid, true) + XCTAssertEqual(report.patchCount, 120) + XCTAssertEqual(report.avgDE, 1.80) + XCTAssertEqual(report.maxDE, 3.50) + XCTAssertEqual(report.rmsDE, 0.95) + XCTAssertEqual(report.status, .good) } - @Test("Broad regex fallback") - func regexFallback() { + func testRegexFallback() { let output = """ No of test patches = 10 avg = 4.25 @@ -45,27 +41,25 @@ struct ProfcheckParserTests { rms = 2.30 """ let report = ProfcheckParser.parse(output) - #expect(report.isValid == true) - #expect(report.avgDE == 4.25) - #expect(report.maxDE == 6.10) - #expect(report.rmsDE == 2.30) - #expect(report.status == .poor) + XCTAssertEqual(report.isValid, true) + XCTAssertEqual(report.avgDE, 4.25) + XCTAssertEqual(report.maxDE, 6.10) + XCTAssertEqual(report.rmsDE, 2.30) + XCTAssertEqual(report.status, .poor) } - @Test("Unparseable output warns, not zeros") - func unparseable() { + func testUnparseable() { let output = "some random text without metrics" let report = ProfcheckParser.parse(output) - #expect(report.isValid == false) - #expect(report.warning != nil) - #expect(report.avgDE == nil) + XCTAssertEqual(report.isValid, false) + XCTAssertNotNil(report.warning) + XCTAssertNil(report.avgDE) } - @Test("Status bands") - func statusBands() { - #expect(VerificationStatus.from(avgDE: 0.5) == .excellent) - #expect(VerificationStatus.from(avgDE: 1.5) == .good) - #expect(VerificationStatus.from(avgDE: 2.5) == .acceptable) - #expect(VerificationStatus.from(avgDE: 4.0) == .poor) + func testStatusBands() { + XCTAssertEqual(VerificationStatus.from(avgDE: 0.5), .excellent) + XCTAssertEqual(VerificationStatus.from(avgDE: 1.5), .good) + XCTAssertEqual(VerificationStatus.from(avgDE: 2.5), .acceptable) + XCTAssertEqual(VerificationStatus.from(avgDE: 4.0), .poor) } } diff --git a/Tests/ICCeryCoreTests/TargenTests.swift b/Tests/ICCeryCoreTests/TargenTests.swift index 821efce..c11a0a1 100644 --- a/Tests/ICCeryCoreTests/TargenTests.swift +++ b/Tests/ICCeryCoreTests/TargenTests.swift @@ -1,12 +1,11 @@ import Testing +import XCTest import Foundation @testable import ICCeryCore -@Suite("TargenArgs") -struct TargenArgsTests { +final class TargenArgsTests: XCTestCase { - @Test("RGB baseline: -v -d 2 -f 800 -e 4 -B 4") - func rgbBaseline() throws { + func testRgbBaseline() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -15,12 +14,11 @@ struct TargenArgsTests { basename: "test_rgb" ) let args = try TargenArgs.build(config: config) - #expect(args == ["-v", "-d", "2", "-f", "800", "-e", "4", "-B", "4", "test_rgb"]) - #expect(!args.contains("-u")) + XCTAssertEqual(args, ["-v", "-d", "2", "-f", "800", "-e", "4", "-B", "4", "test_rgb"]) + XCTAssertFalse(args.contains("-u")) } - @Test("CMYK baseline: -v -d 4 -f 1500 -e 4 -B 0") - func cmykBaseline() throws { + func testCmykBaseline() throws { let config = TargenConfig( colourSpace: .cmyk, patchCount: 1500, @@ -29,11 +27,10 @@ struct TargenArgsTests { basename: "test_cmyk" ) let args = try TargenArgs.build(config: config) - #expect(args == ["-v", "-d", "4", "-f", "1500", "-e", "4", "-B", "0", "test_cmyk"]) + XCTAssertEqual(args, ["-v", "-d", "4", "-f", "1500", "-e", "4", "-B", "0", "test_cmyk"]) } - @Test("Custom patch count honours -f (#44)") - func customPatchCount() throws { + func testCustomPatchCount() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 2500, @@ -42,12 +39,11 @@ struct TargenArgsTests { basename: "custom_patches" ) let args = try TargenArgs.build(config: config) - #expect(args.contains("-f")) - #expect(args[args.firstIndex(of: "-f")! + 1] == "2500") + XCTAssertTrue(args.contains("-f")) + XCTAssertEqual(args[args.firstIndex(of: "-f")! + 1], "2500") } - @Test("All advanced flags in stable order") - func allAdvancedFlags() throws { + func testAllAdvancedFlags() throws { let config = TargenConfig( colourSpace: .cmyk, patchCount: 1200, @@ -85,11 +81,10 @@ struct TargenArgsTests { "-p", "2.00", "advanced_cmyk" ] - #expect(args == expected) + XCTAssertEqual(args, expected) } - @Test("RGB ignores total ink limit") - func rgbIgnoresInkLimit() throws { + func testRgbIgnoresInkLimit() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -99,11 +94,10 @@ struct TargenArgsTests { basename: "rgb_no_ink" ) let args = try TargenArgs.build(config: config) - #expect(!args.contains("-l")) + XCTAssertFalse(args.contains("-l")) } - @Test("Neutral concentration omitted when approximately 0.50") - func neutralConcentrationOmittedWhenDefault() throws { + func testNeutralConcentrationOmittedWhenDefault() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -113,11 +107,10 @@ struct TargenArgsTests { basename: "n_default" ) let args = try TargenArgs.build(config: config) - #expect(!args.contains("-N")) + XCTAssertFalse(args.contains("-N")) } - @Test("Adaptation emitted even at 0.10 (no default-skip)") - func adaptationEmittedAtPointOne() throws { + func testAdaptationEmittedAtPointOne() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -127,12 +120,11 @@ struct TargenArgsTests { basename: "a_flag" ) let args = try TargenArgs.build(config: config) - #expect(args.contains("-A")) - #expect(args[args.firstIndex(of: "-A")! + 1] == "0.10") + XCTAssertTrue(args.contains("-A")) + XCTAssertEqual(args[args.firstIndex(of: "-A")! + 1], "0.10") } - @Test("OFPS full spread algorithm emits no flag") - func ofpsEmitsNoFlag() throws { + func testOfpsEmitsNoFlag() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -142,12 +134,11 @@ struct TargenArgsTests { basename: "ofps_test" ) let args = try TargenArgs.build(config: config) - #expect(!args.contains("ofps")) - #expect(!args.contains("-t")) + XCTAssertFalse(args.contains("ofps")) + XCTAssertFalse(args.contains("-t")) } - @Test("Dark emphasis and device power omitted when 1.0") - func darkEmphasisAndPowerOmittedWhenOne() throws { + func testDarkEmphasisAndPowerOmittedWhenOne() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -158,12 +149,11 @@ struct TargenArgsTests { basename: "defaults_omitted" ) let args = try TargenArgs.build(config: config) - #expect(!args.contains("-V")) - #expect(!args.contains("-p")) + XCTAssertFalse(args.contains("-V")) + XCTAssertFalse(args.contains("-p")) } - @Test("Whitespace-only preconditioning profile emits no -c") - func whitespacePreconditioner() throws { + func testWhitespacePreconditioner() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -173,11 +163,10 @@ struct TargenArgsTests { basename: "ws_pre" ) let args = try TargenArgs.build(config: config) - #expect(!args.contains("-c")) + XCTAssertFalse(args.contains("-c")) } - @Test("Preconditioning profile is trimmed before emission") - func preconditionerTrimmed() throws { + func testPreconditionerTrimmed() throws { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -187,11 +176,10 @@ struct TargenArgsTests { basename: "trim_pre" ) let args = try TargenArgs.build(config: config) - #expect(args[args.firstIndex(of: "-c")! + 1] == "/path/to/profile.icc") + XCTAssertEqual(args[args.firstIndex(of: "-c")! + 1], "/path/to/profile.icc") } - @Test("Invalid basename throws") - func invalidBasenameThrows() { + func testInvalidBasenameThrows() { let config = TargenConfig( colourSpace: .rgb, patchCount: 800, @@ -199,13 +187,12 @@ struct TargenArgsTests { blackPatches: 4, basename: "../bad_name" ) - #expect(throws: PathSecurity.Error.self) { - try TargenArgs.build(config: config) + XCTAssertThrowsError(try TargenArgs.build(config: config)) { error in + XCTAssertTrue(error is PathSecurity.Error) } } - @Test("Invalid patch count throws") - func invalidPatchCountThrows() { + func testInvalidPatchCountThrows() { let config = TargenConfig( colourSpace: .rgb, patchCount: 0, @@ -213,13 +200,12 @@ struct TargenArgsTests { blackPatches: 4, basename: "bad_count" ) - #expect(throws: TargenArgError.self) { - try TargenArgs.build(config: config) + XCTAssertThrowsError(try TargenArgs.build(config: config)) { error in + XCTAssertTrue(error is TargenArgError) } } - @Test("Invalid ink limit throws for CMYK") - func invalidInkLimitThrows() { + func testInvalidInkLimitThrows() { let config = TargenConfig( colourSpace: .cmyk, patchCount: 800, @@ -228,8 +214,8 @@ struct TargenArgsTests { totalInkLimit: 450, basename: "bad_ink" ) - #expect(throws: TargenArgError.self) { - try TargenArgs.build(config: config) + XCTAssertThrowsError(try TargenArgs.build(config: config)) { error in + XCTAssertTrue(error is TargenArgError) } } } diff --git a/Tests/ICCeryCoreTests/TestAppEnvironment.swift b/Tests/ICCeryCoreTests/TestAppEnvironment.swift index ac27fe4..1a7f9d0 100644 --- a/Tests/ICCeryCoreTests/TestAppEnvironment.swift +++ b/Tests/ICCeryCoreTests/TestAppEnvironment.swift @@ -1,4 +1,3 @@ -import Testing import Foundation @testable import ICCeryCore @testable import ICCery diff --git a/Tests/ICCeryCoreTests/XCTestCase+AsyncThrows.swift b/Tests/ICCeryCoreTests/XCTestCase+AsyncThrows.swift new file mode 100644 index 0000000..c26ea15 --- /dev/null +++ b/Tests/ICCeryCoreTests/XCTestCase+AsyncThrows.swift @@ -0,0 +1,21 @@ +import XCTest + +extension XCTestCase { + func assertAsyncThrows( + expectedType: E.Type, + _ expression: () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line, + errorHandler: ((E) -> Void)? = nil + ) async { + do { + _ = try await expression() + XCTFail("Expected \(expectedType) to be thrown but expression succeeded. \(message())", file: file, line: line) + } catch let error as E { + errorHandler?(error) + } catch { + XCTFail("Expected \(expectedType) but caught \(type(of: error)): \(error). \(message())", file: file, line: line) + } + } +}