Files
iccery-v2-mac/Tests/ICCeryCoreTests/GamutMeshParserTests.swift
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 0ee609be1c feat(#28): SceneKit gamut viewer
- Add native .gam mesh parser and mesh model (GamutMeshParser, GamutMesh).
- Add GamutView / GamutViewModel with SceneKit, bundled sRGB.gam reference,
  and profile-gamut comparison.
- Wire Stage 5 "View Gamut" button and sidebar quick action.
- Extract a best-effort .gam mesh after colprof via Iccgamut in
  ProfileWorkflowViewModel, with artefact gating for the generated .gam.
- Add BinaryResolver.referenceGamut() for bundled reference gamuts.
- Add Milestone6GamutUITests and GamutMeshParserTests.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-09 22:06:19 +01:00

167 lines
4.5 KiB
Swift

import Foundation
import Testing
@testable import ICCeryCore
/// ``GamutMeshParser`` acceptance + edge-case tests.
@Suite("Gamut mesh parser")
struct GamutMeshParserTests {
/// Returns the bundled real `sRGB.gam` in `Resources/Argyll/reference_gamuts`.
private var bundledSRGBGamURL: URL {
let bundle = Bundle.main
let resource = bundle.resourceURL ?? bundle.bundleURL
return resource.appendingPathComponent("Argyll/reference_gamuts/sRGB.gam")
}
@Test("Parses bundled sRGB.gam")
func parsesBundledSRGB() 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")
}
@Test("Discards VERTEX_NO and uses push-order indices")
func discardsVertexNo() throws {
let text = """
GAMUT
NUMBER_OF_FIELDS 4
BEGIN_DATA_FORMAT
VERTEX_NO LAB_L LAB_A LAB_B
END_DATA_FORMAT
NUMBER_OF_SETS 4
BEGIN_DATA
100 10.0 20.0 30.0
50 20.0 30.0 40.0
2 30.0 40.0 50.0
7 40.0 50.0 60.0
END_DATA
NUMBER_OF_FIELDS 3
BEGIN_DATA_FORMAT
VERTEX_0 VERTEX_1 VERTEX_2
END_DATA_FORMAT
NUMBER_OF_SETS 2
BEGIN_DATA
0 1 2
1 2 3
END_DATA
"""
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))
}
@Test("Ignores comments and blank lines")
func ignoresComments() throws {
let text = """
# Header comment
NUMBER_OF_FIELDS 4
BEGIN_DATA_FORMAT
VERTEX_NO LAB_L LAB_A LAB_B
END_DATA_FORMAT
NUMBER_OF_SETS 2
BEGIN_DATA
0 10.0 20.0 30.0
# inline comment
1 20.0 30.0 40.0
END_DATA
# another comment
NUMBER_OF_FIELDS 3
BEGIN_DATA_FORMAT
VERTEX_0 VERTEX_1 VERTEX_2
END_DATA_FORMAT
NUMBER_OF_SETS 1
BEGIN_DATA
0 1 0
END_DATA
"""
let mesh = try GamutMeshParser.parse(text: text)
#expect(mesh.vertices.count == 2)
#expect(mesh.faces.count == 1)
}
@Test("Remaps coordinates to x=a*, y=L*, z=b*")
func remapsCoordinates() throws {
let text = """
NUMBER_OF_FIELDS 4
BEGIN_DATA_FORMAT
VERTEX_NO LAB_L LAB_A LAB_B
END_DATA_FORMAT
NUMBER_OF_SETS 1
BEGIN_DATA
0 50.0 -20.0 80.0
END_DATA
"""
let mesh = try GamutMeshParser.parse(text: text)
#expect(mesh.vertices.first?.position == SIMD3<Float>(-20, 50, 80))
}
@Test("Computes per-vertex sRGB colour")
func computesVertexColor() throws {
let text = """
NUMBER_OF_FIELDS 4
BEGIN_DATA_FORMAT
VERTEX_NO LAB_L LAB_A LAB_B
END_DATA_FORMAT
NUMBER_OF_SETS 1
BEGIN_DATA
0 100.0 0.0 0.0
END_DATA
"""
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)
}
@Test("Drops out-of-bounds face indices")
func dropsOutOfBoundsFaces() throws {
let text = """
NUMBER_OF_FIELDS 4
BEGIN_DATA_FORMAT
VERTEX_NO LAB_L LAB_A LAB_B
END_DATA_FORMAT
NUMBER_OF_SETS 2
BEGIN_DATA
0 10.0 0.0 0.0
1 20.0 0.0 0.0
END_DATA
NUMBER_OF_FIELDS 3
BEGIN_DATA_FORMAT
VERTEX_0 VERTEX_1 VERTEX_2
END_DATA_FORMAT
NUMBER_OF_SETS 2
BEGIN_DATA
0 1 0
0 1 99
END_DATA
"""
let mesh = try GamutMeshParser.parse(text: text)
#expect(mesh.faces.count == 1)
}
@Test("Throws on empty file")
func throwsOnEmptyFile() {
#expect(throws: GamutMeshParseError.noDataBlock) {
_ = try GamutMeshParser.parse(text: "")
}
}
@Test("Throws when file is missing")
func throwsWhenMissing() {
let url = URL(fileURLWithPath: "/nonexistent/path/to/mesh.gam")
#expect(throws: GamutMeshParseError.missingFile) {
_ = try GamutMeshParser.parse(url: url)
}
}
}