From cfb0811ce0469ccc546458ebf16f4189c2ad81fd Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Tue, 4 Aug 2026 09:03:44 +0900 Subject: [PATCH] Make the sprite goldens reproducible off a P3 Mac The two canvas goldens added with `TortoiseSprite` passed locally but failed on CI with a pure color shift (perceptual precision 0.956, geometry intact); the 16 pre-existing scenario goldens were unaffected. The sprite image was rendered from a SwiftUI view via `ImageRenderer`, which made it depend on the host machine twice over: `Color.red` / `Color.blue` are semantic colors whose components vary with display gamut and appearance, and `ImageRenderer.nsImage` returns a bitmap tagged with the host's display profile. The scenario goldens escaped this because they only draw vector paths in explicitly-constructed sRGB colors. Build the sprite from raw sRGB bytes through `CGImage` + `Image(decorative:scale:)` instead, so no color resolution happens. The recorded pixels now come out byte-identical to the values written in. Also pin the tests to `.tortoiseViewport(.original)` and size the source bitmaps so they map 1:1 onto device pixels (80x80 px at scale 2 into a 40x40 box; 160x80 into 80x80), removing image resampling as a second source of cross-machine drift. Both goldens were re-recorded and visually inspected. --- .../TortoiseUITests/TortoiseSpriteTests.swift | 69 +++++++++++++----- .../TortoiseSpriteTests/imageSprite.1.png | Bin 14699 -> 14351 bytes .../imageSpriteAspectRatio.1.png | Bin 14584 -> 14428 bytes 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/Tests/TortoiseUITests/TortoiseSpriteTests.swift b/Tests/TortoiseUITests/TortoiseSpriteTests.swift index 55511ec..7dfb4d9 100644 --- a/Tests/TortoiseUITests/TortoiseSpriteTests.swift +++ b/Tests/TortoiseUITests/TortoiseSpriteTests.swift @@ -1,4 +1,5 @@ import CoreGraphics +import Foundation import SwiftUI import Testing import TortoiseCore @@ -66,25 +67,51 @@ extension CGSize { @Suite("Tortoise sprite canvas snapshots") @MainActor struct TortoiseSpriteCanvasTests { - /// An asymmetric, up-pointing sprite built in code (no test resource): - /// a red top half over a blue bottom half, so a golden image shows both - /// that the artwork is drawn and which way it is rotated. - private static func spriteImage(width: Double, height: Double) -> Image? { - let content = VStack(spacing: 0) { - Rectangle().fill(Color.red) - Rectangle().fill(Color.blue) + /// An asymmetric, up-pointing sprite: a red top half over a blue bottom + /// half, so a golden image shows both that the artwork is drawn and + /// which way it is rotated. + /// + /// The pixels are written out explicitly in sRGB rather than rendered + /// from a SwiftUI view. `ImageRenderer` was used at first and made the + /// goldens machine-dependent: `Color.red` / `Color.blue` are semantic + /// colors whose components depend on the display gamut and appearance, + /// and `ImageRenderer.nsImage` hands back a bitmap tagged with the + /// host's display profile — so the goldens recorded on a P3 Mac failed + /// on CI with a pure color shift (perceptual precision 0.956). + private static func spriteImage(pixelWidth: Int, pixelHeight: Int) -> Image? { + let bytesPerPixel = 4 + var pixels = [UInt8](repeating: 0, count: pixelWidth * pixelHeight * bytesPerPixel) + for y in 0..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 J&nM;W_y_%OlIj2e diff --git a/Tests/TortoiseUITests/__Snapshots__/TortoiseSpriteTests/imageSpriteAspectRatio.1.png b/Tests/TortoiseUITests/__Snapshots__/TortoiseSpriteTests/imageSpriteAspectRatio.1.png index c01e3d537cffec7f30186bb12ab3d422caf76f7a..3a834f509a08bad903699268fc912ab8a5caecc5 100644 GIT binary patch literal 14428 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 XHA$E9vjSH;|3R3Rl#$r5RId0NPyMH}