Stage 1 targen: typed TargenConfig + argv builder (-v -d 2/4, -f, -e, -B, optional advanced flags, CMYK-only -l, never -u), ArgyllRunner over ProcessManager, Stage1View with disk-gated Generate. Issue 8: Open Existing resume — .ti1 → Stage 2, .ti2 → Stage 3 shell with persistent "Resumed from .ti2" banner, sibling-.ti1 gate. Stage 2 printtarg: PrinttargConfig/args (-v -u, -i, -p, -R 1 default, -r raster, -t/-T DPI), Stage2View with instrument/page/DPI/label controls, CM warning, stubbed print panel. Issue 10: pretty-manifest JSON parsing, host-side TIFF→PNG gallery previews, per-page stubbed print, failure stays on stage. Issue 11: typed ProfilingPreset schema with legacy CustomPreset migration, four built-ins, save/manage/import/export UI, DPI binding. Tests: 124 Core tests (Swift Testing) + 11 Milestone2UITests (XCTest) with committed fixture sidecars — all green on arm64+x86_64 universal build. No hardware, no downloads. Also: ProcessError LocalizedError conformance; accessibilityElement .contain on container identifiers so child AX ids survive. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
331 lines
11 KiB
Swift
331 lines
11 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import ICCeryCore
|
|
|
|
@Suite("TargenArgs")
|
|
struct TargenArgsTests {
|
|
|
|
@Test("RGB baseline: -v -d 2 -f 800 -e 4 -B 4")
|
|
func rgbBaseline() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
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"))
|
|
}
|
|
|
|
@Test("CMYK baseline: -v -d 4 -f 1500 -e 4 -B 0")
|
|
func cmykBaseline() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .cmyk,
|
|
patchCount: 1500,
|
|
whitePatches: 4,
|
|
blackPatches: 0,
|
|
basename: "test_cmyk"
|
|
)
|
|
let args = try TargenArgs.build(config: config)
|
|
#expect(args == ["-v", "-d", "4", "-f", "1500", "-e", "4", "-B", "0", "test_cmyk"])
|
|
}
|
|
|
|
@Test("Custom patch count honours -f (#44)")
|
|
func customPatchCount() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 2500,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
basename: "custom_patches"
|
|
)
|
|
let args = try TargenArgs.build(config: config)
|
|
#expect(args.contains("-f"))
|
|
#expect(args[args.firstIndex(of: "-f")! + 1] == "2500")
|
|
}
|
|
|
|
@Test("All advanced flags in stable order")
|
|
func allAdvancedFlags() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .cmyk,
|
|
patchCount: 1200,
|
|
whitePatches: 6,
|
|
blackPatches: 2,
|
|
greySteps: 12,
|
|
singleChannelSteps: 8,
|
|
neutralSteps: 6,
|
|
neutralConcentration: 0.75,
|
|
preconditioningProfile: "/path/to/profile.icc",
|
|
ofpsHighQuality: true,
|
|
ofpsAdaptation: 0.10,
|
|
fullSpreadAlgorithm: .target,
|
|
totalInkLimit: 320,
|
|
darkEmphasis: 1.50,
|
|
devicePower: 2.0,
|
|
basename: "advanced_cmyk"
|
|
)
|
|
let args = try TargenArgs.build(config: config)
|
|
let expected = [
|
|
"-v", "-d", "4",
|
|
"-f", "1200",
|
|
"-e", "6",
|
|
"-B", "2",
|
|
"-g", "12",
|
|
"-s", "8",
|
|
"-n", "6",
|
|
"-N", "0.75",
|
|
"-c", "/path/to/profile.icc",
|
|
"-G",
|
|
"-A", "0.10",
|
|
"-t",
|
|
"-l", "320",
|
|
"-V", "1.50",
|
|
"-p", "2.00",
|
|
"advanced_cmyk"
|
|
]
|
|
#expect(args == expected)
|
|
}
|
|
|
|
@Test("RGB ignores total ink limit")
|
|
func rgbIgnoresInkLimit() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
totalInkLimit: 300,
|
|
basename: "rgb_no_ink"
|
|
)
|
|
let args = try TargenArgs.build(config: config)
|
|
#expect(!args.contains("-l"))
|
|
}
|
|
|
|
@Test("Neutral concentration omitted when approximately 0.50")
|
|
func neutralConcentrationOmittedWhenDefault() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
neutralConcentration: 0.5005,
|
|
basename: "n_default"
|
|
)
|
|
let args = try TargenArgs.build(config: config)
|
|
#expect(!args.contains("-N"))
|
|
}
|
|
|
|
@Test("Adaptation emitted even at 0.10 (no default-skip)")
|
|
func adaptationEmittedAtPointOne() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
ofpsAdaptation: 0.10,
|
|
basename: "a_flag"
|
|
)
|
|
let args = try TargenArgs.build(config: config)
|
|
#expect(args.contains("-A"))
|
|
#expect(args[args.firstIndex(of: "-A")! + 1] == "0.10")
|
|
}
|
|
|
|
@Test("OFPS full spread algorithm emits no flag")
|
|
func ofpsEmitsNoFlag() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
fullSpreadAlgorithm: .ofps,
|
|
basename: "ofps_test"
|
|
)
|
|
let args = try TargenArgs.build(config: config)
|
|
#expect(!args.contains("ofps"))
|
|
#expect(!args.contains("-t"))
|
|
}
|
|
|
|
@Test("Dark emphasis and device power omitted when 1.0")
|
|
func darkEmphasisAndPowerOmittedWhenOne() throws {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
darkEmphasis: 1.0,
|
|
devicePower: 1.0,
|
|
basename: "defaults_omitted"
|
|
)
|
|
let args = try TargenArgs.build(config: config)
|
|
#expect(!args.contains("-V"))
|
|
#expect(!args.contains("-p"))
|
|
}
|
|
|
|
@Test("Invalid basename throws")
|
|
func invalidBasenameThrows() {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
basename: "../bad_name"
|
|
)
|
|
#expect(throws: PathSecurity.Error.self) {
|
|
try TargenArgs.build(config: config)
|
|
}
|
|
}
|
|
|
|
@Test("Invalid patch count throws")
|
|
func invalidPatchCountThrows() {
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 0,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
basename: "bad_count"
|
|
)
|
|
#expect(throws: TargenArgError.self) {
|
|
try TargenArgs.build(config: config)
|
|
}
|
|
}
|
|
|
|
@Test("Invalid ink limit throws for CMYK")
|
|
func invalidInkLimitThrows() {
|
|
let config = TargenConfig(
|
|
colourSpace: .cmyk,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 0,
|
|
totalInkLimit: 450,
|
|
basename: "bad_ink"
|
|
)
|
|
#expect(throws: TargenArgError.self) {
|
|
try TargenArgs.build(config: config)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Suite("ArgyllRunner Targen")
|
|
struct ArgyllRunnerTargenTests {
|
|
|
|
@Test("Successful targen execution creates .ti1 and returns URL")
|
|
func successfulTargenExecution() async throws {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: tempDir) }
|
|
|
|
// Create a mock targen script
|
|
let mockScript = """
|
|
#!/bin/sh
|
|
# Find the last argument which is the basename
|
|
for arg do shift; set -- "$@" "$arg"; done
|
|
last="$arg"
|
|
echo "Generating patches..."
|
|
touch "$last.ti1"
|
|
echo "Done!"
|
|
exit 0
|
|
"""
|
|
let mockURL = tempDir.appendingPathComponent("targen")
|
|
try mockScript.write(to: mockURL, atomically: true, encoding: .utf8)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: mockURL.path)
|
|
|
|
let resolver = BinaryResolver(bundledRoot: tempDir, overrideDir: tempDir)
|
|
let pm = ProcessManager()
|
|
let runner = ArgyllRunner(processManager: pm, binaryResolver: resolver)
|
|
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
basename: "mock_test",
|
|
workingDirectory: tempDir
|
|
)
|
|
|
|
var logLines: [String] = []
|
|
final class LogBox: @unchecked Sendable {
|
|
var lines: [String] = []
|
|
let lock = NSLock()
|
|
func append(_ batch: [String]) {
|
|
lock.lock(); lines.append(contentsOf: batch); lock.unlock()
|
|
}
|
|
}
|
|
let box = LogBox()
|
|
let ti1URL = try await runner.runTargen(config: config) { batch in
|
|
box.append(batch)
|
|
}
|
|
logLines = box.lines
|
|
#expect(logLines.contains("Generating patches..."))
|
|
|
|
#expect(FileManager.default.fileExists(atPath: ti1URL.path))
|
|
#expect(ti1URL.lastPathComponent == "mock_test.ti1")
|
|
}
|
|
|
|
@Test("Failed targen execution throws processFailed")
|
|
func failedTargenExecution() async throws {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: tempDir) }
|
|
|
|
let mockScript = """
|
|
#!/bin/sh
|
|
echo "Error: something went wrong" >&2
|
|
exit 1
|
|
"""
|
|
let mockURL = tempDir.appendingPathComponent("targen")
|
|
try mockScript.write(to: mockURL, atomically: true, encoding: .utf8)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: mockURL.path)
|
|
|
|
let resolver = BinaryResolver(bundledRoot: tempDir, overrideDir: tempDir)
|
|
let pm = ProcessManager()
|
|
let runner = ArgyllRunner(processManager: pm, binaryResolver: resolver)
|
|
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
basename: "fail_test",
|
|
workingDirectory: tempDir
|
|
)
|
|
|
|
await #expect(throws: ArgyllRunnerError.self) {
|
|
try await runner.runTargen(config: config)
|
|
}
|
|
}
|
|
|
|
@Test("Targen exit 0 without .ti1 throws missingArtefact")
|
|
func missingArtefactThrows() async throws {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: tempDir) }
|
|
|
|
let mockScript = """
|
|
#!/bin/sh
|
|
echo "Exited 0 but did not create file"
|
|
exit 0
|
|
"""
|
|
let mockURL = tempDir.appendingPathComponent("targen")
|
|
try mockScript.write(to: mockURL, atomically: true, encoding: .utf8)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: mockURL.path)
|
|
|
|
let resolver = BinaryResolver(bundledRoot: tempDir, overrideDir: tempDir)
|
|
let pm = ProcessManager()
|
|
let runner = ArgyllRunner(processManager: pm, binaryResolver: resolver)
|
|
|
|
let config = TargenConfig(
|
|
colourSpace: .rgb,
|
|
patchCount: 800,
|
|
whitePatches: 4,
|
|
blackPatches: 4,
|
|
basename: "no_file",
|
|
workingDirectory: tempDir
|
|
)
|
|
|
|
await #expect(throws: ArgyllRunnerError.self) {
|
|
try await runner.runTargen(config: config)
|
|
}
|
|
}
|
|
}
|