From 17805b1b1feac96e4adcf43b2d0c5909ca564b8c Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Mon, 14 Sep 2026 20:50:45 +0900 Subject: [PATCH 1/6] Require Swift 6.4 / Xcode 27 Bump swift-tools-version to 6.4 and move CI to the xcode-27 runner image. The Linux job uses the 6.4 release-branch nightly until the official swift:6.4 image is published. No source changes were needed: the package builds without warnings and passes all tests under Swift 6.4. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/actions/select-xcode/action.yml | 2 +- .github/workflows/ci.yml | 10 ++++++---- .github/workflows/docs.yml | 2 +- CHANGELOG.md | 9 +++++++++ CONTRIBUTING.md | 7 ++++--- Package.swift | 2 +- README.md | 6 +++--- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4ce9003..3d9e541 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -66,6 +66,6 @@ body: attributes: label: Environment description: OS and version, Xcode version. - placeholder: "macOS 26.1, Xcode 26.0" + placeholder: "macOS 27.0, Xcode 27.0" validations: required: true diff --git a/.github/actions/select-xcode/action.yml b/.github/actions/select-xcode/action.yml index 06d919b..54e8af4 100644 --- a/.github/actions/select-xcode/action.yml +++ b/.github/actions/select-xcode/action.yml @@ -7,7 +7,7 @@ description: > inputs: xcode-major: description: Xcode major version to select - default: "26" + default: "27" runs: using: composite diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d66744c..1c0a14e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: jobs: build-test: name: Build & Test - runs-on: macos-15 + runs-on: xcode-27 permissions: contents: read id-token: write # Codecov upload via GitHub OIDC (no static token) @@ -54,7 +54,7 @@ jobs: # parallel job, so the test job doesn't build everything twice. release-build: name: Release Build - runs-on: macos-15 + runs-on: xcode-27 steps: - uses: actions/checkout@v7 @@ -76,7 +76,9 @@ jobs: linux: name: Linux (Core + SVG) runs-on: ubuntu-latest - container: swift:6.2 + # TODO: switch to the official `swift:6.4` image once it is published; + # until then, the 6.4 release-branch nightly is the only 6.4 toolchain. + container: swiftlang/swift:nightly-6.4.x-noble steps: - uses: actions/checkout@v7 @@ -92,7 +94,7 @@ jobs: format-lint: name: swift-format lint - runs-on: macos-15 + runs-on: xcode-27 steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e2f5789..25003b6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,7 +18,7 @@ concurrency: jobs: build: name: Build DocC - runs-on: macos-15 + runs-on: xcode-27 steps: - uses: actions/checkout@v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index ad0c491..c8de887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +### Changed +- Requires Swift 6.4+ / Xcode 27+ (was Swift 6.2+ / Xcode 26+) — the package + manifest now declares `swift-tools-version: 6.4`. No source changes were + needed: the package builds without warnings and passes all tests under + Swift 6.4. Platform requirements are unchanged (iOS 26+ · macOS 26+ · + visionOS 26+ · Linux) + ## 2.1.0 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ceba104..f8cab69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ requests, documentation fixes, and pull requests are all welcome. ## Requirements -- **Swift** 6.2+ / **Xcode** 26+ +- **Swift** 6.4+ / **Xcode** 27+ - **macOS** to run the full test suite — the golden image tests in `TortoiseUITests` render a SwiftUI view and are macOS-only. @@ -73,10 +73,11 @@ Rules: visible output — add or update a scenario so both renderers stay covered. The PNG goldens depend on OS-level rendering. The current set was recorded on -**macOS 26** (Xcode 26); CI compares them on `macos-15` runners, with a small +**macOS 26** (Xcode 26); CI compares them on the `xcode-27` runner image +(macOS 27), with a small tolerance (`precision: 0.995`, `perceptualPrecision: 0.98`) absorbing antialiasing drift between OS versions. If a PR bumps the CI runner image -(`runs-on: macos-XX`) or you re-record on a different macOS version, confirm +(`runs-on:`) or you re-record on a different macOS version, confirm the PNG golden tests still pass in CI — and if they don't, re-record both golden sets and visually inspect them as described above. diff --git a/Package.swift b/Package.swift index da5d70d..49e2fef 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.2 +// swift-tools-version: 6.4 import PackageDescription var products: [Product] = [ diff --git a/README.md b/README.md index 5fd18af..60ad5c2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # TortoiseGraphics2 -[![Swift](https://img.shields.io/badge/Swift-6.2-orange.svg)](https://swift.org) +[![Swift](https://img.shields.io/badge/Swift-6.4-orange.svg)](https://swift.org) [![Swift Package Manager](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://swift.org/package-manager) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![codecov](https://codecov.io/gh/temoki/TortoiseGraphics2/graph/badge.svg)](https://codecov.io/gh/temoki/TortoiseGraphics2) @@ -68,8 +68,8 @@ and testing all share a single source of truth. ## Requirements -- **Swift** 6.2+ -- **Xcode** 26+ (Apple platforms) +- **Swift** 6.4+ +- **Xcode** 27+ (Apple platforms) - **Platforms** iOS 26+ · macOS 26+ · visionOS 26+ · Linux (`TortoiseCore` / `TortoiseSVG` only — `TortoiseUI` requires SwiftUI) ## Installation From 25982e392d62ef5458d0508e8ee2b63d633d46cb Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Mon, 14 Sep 2026 20:58:21 +0900 Subject: [PATCH 2/6] Compare canvas goldens byte-wise to avoid a macOS 27 crash swift-snapshot-testing's perceptual comparison passes a bare CGRect as CIAreaAverage's inputExtent, which Core Image on macOS 27 rejects with an uncaught -[NSConcreteValue CGRectValue] exception, killing the whole TortoiseUITests process on the xcode-27 runner. Byte-wise comparison with the same 0.995 precision never enters that path. --- CLAUDE.md | 2 +- CONTRIBUTING.md | 6 ++++-- .../CanvasGoldenSnapshotting.swift | 19 +++++++++++++++++++ .../DrawingScenarioCanvasTests.swift | 2 +- .../TortoiseUITests/TortoiseSpriteTests.swift | 2 +- 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift diff --git a/CLAUDE.md b/CLAUDE.md index d023721..86eee99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,7 +91,7 @@ SVG tests avoid raw string literals (`#"..."#`) when the expected string contain **Drawing-scenario golden tests.** `Tests/TortoiseTestSupport/` (a non-product target) defines `DrawingScenario.all` — feature-grouped tortoise programs that together cover every `TortoiseCommand` case. Both renderers are verified against the same scenarios via pointfreeco/swift-snapshot-testing: - `TortoiseSVGTests/DrawingScenarioSVGTests` compares full SVG strings against goldens in `Tests/TortoiseSVGTests/__Snapshots__/`. -- `TortoiseUITests/DrawingScenarioCanvasTests` (macOS-only, `#if os(macOS)`) renders `TortoiseCanvas` via `ImageRenderer` (`scale = 2`, instant mode forced by prepending `speed = 0`) and compares PNGs in `Tests/TortoiseUITests/__Snapshots__/` with `precision: 0.995, perceptualPrecision: 0.98` to absorb OS-level antialiasing drift. +- `TortoiseUITests/DrawingScenarioCanvasTests` (macOS-only, `#if os(macOS)`) renders `TortoiseCanvas` via `ImageRenderer` (`scale = 2`, instant mode forced by prepending `speed = 0`) and compares PNGs in `Tests/TortoiseUITests/__Snapshots__/` with `.canvasGolden` (byte-wise `precision: 0.995`, defined in `CanvasGoldenSnapshotting.swift`) to absorb OS-level antialiasing drift. **Do not add `perceptualPrecision`**: swift-snapshot-testing's perceptual path passes a bare `CGRect` to `CIAreaAverage`, which Core Image on macOS 27 rejects with an uncaught exception that kills the whole test process. It only triggers when bytes differ, so it passes on the recording macOS and crashes on the CI runner's. After intentionally changing scenario programs or renderer output, re-record **both** golden sets in the same commit and visually inspect them: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8cab69..aa9cc44 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,8 +75,10 @@ Rules: The PNG goldens depend on OS-level rendering. The current set was recorded on **macOS 26** (Xcode 26); CI compares them on the `xcode-27` runner image (macOS 27), with a small -tolerance (`precision: 0.995`, `perceptualPrecision: 0.98`) absorbing -antialiasing drift between OS versions. If a PR bumps the CI runner image +byte-wise tolerance (`precision: 0.995`, defined once as `.canvasGolden` in +`Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift`) absorbing +antialiasing drift between OS versions. Don't add `perceptualPrecision`: that +comparison crashes the test process on macOS 27. If a PR bumps the CI runner image (`runs-on:`) or you re-record on a different macOS version, confirm the PNG golden tests still pass in CI — and if they don't, re-record both golden sets and visually inspect them as described above. diff --git a/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift b/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift new file mode 100644 index 0000000..5b47c34 --- /dev/null +++ b/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift @@ -0,0 +1,19 @@ +#if os(macOS) + import AppKit + import SnapshotTesting + + extension Snapshotting where Value == NSImage, Format == NSImage { + /// The comparison every canvas PNG golden uses. + /// + /// Byte-wise with a small tolerance, deliberately *without* + /// `perceptualPrecision`. swift-snapshot-testing's perceptual path passes a + /// bare `CGRect` as `CIAreaAverage`'s `inputExtent`, which Core Image on + /// macOS 27 rejects with an uncaught `-[NSConcreteValue CGRectValue]` + /// exception — crashing the whole test process rather than failing one + /// test. An exact match never reaches that path, which is why the goldens + /// still pass on the macOS they were recorded on. + static var canvasGolden: Snapshotting { + .image(precision: 0.995) + } + } +#endif diff --git a/Tests/TortoiseUITests/DrawingScenarioCanvasTests.swift b/Tests/TortoiseUITests/DrawingScenarioCanvasTests.swift index b5aa61d..9b2a4d1 100644 --- a/Tests/TortoiseUITests/DrawingScenarioCanvasTests.swift +++ b/Tests/TortoiseUITests/DrawingScenarioCanvasTests.swift @@ -30,7 +30,7 @@ } assertSnapshot( of: image, - as: .image(precision: 0.995, perceptualPrecision: 0.98), + as: .canvasGolden, named: scenario.name, testName: "scenario" ) diff --git a/Tests/TortoiseUITests/TortoiseSpriteTests.swift b/Tests/TortoiseUITests/TortoiseSpriteTests.swift index 964f972..ad57fc8 100644 --- a/Tests/TortoiseUITests/TortoiseSpriteTests.swift +++ b/Tests/TortoiseUITests/TortoiseSpriteTests.swift @@ -183,7 +183,7 @@ extension CGSize { return } assertSnapshot( - of: image, as: .image(precision: 0.995, perceptualPrecision: 0.98), + of: image, as: .canvasGolden, fileID: fileID, file: filePath, testName: function, line: line, column: column) } } From 5b858d9f4fd50fe42835687f93cda29c90cc519f Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Mon, 14 Sep 2026 21:01:40 +0900 Subject: [PATCH 3/6] Export coverage from every per-target test bundle SwiftPM in Xcode 27 builds one .xctest bundle per test target instead of a single TortoiseGraphicsPackageTests.xctest, so the lcov export failed with "No such file or directory" after all tests had passed. Pass each bundle's binary to llvm-cov with -object. --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c0a14e..12dee7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,13 +34,19 @@ jobs: - name: Test run: swift test --enable-code-coverage + # SwiftPM builds one .xctest bundle per test target (not a single + # PackageTests.xctest), so pass every bundle's binary to llvm-cov. - name: Export coverage (lcov) run: | BIN_PATH=$(swift build --show-bin-path) + objects=() + for bundle in "$BIN_PATH"/*.xctest; do + objects+=(-object "$bundle/Contents/MacOS/$(basename "$bundle" .xctest)") + done xcrun llvm-cov export -format=lcov \ -instr-profile "$BIN_PATH/codecov/default.profdata" \ -ignore-filename-regex="\.build|Tests" \ - "$BIN_PATH/TortoiseGraphicsPackageTests.xctest/Contents/MacOS/TortoiseGraphicsPackageTests" \ + "${objects[@]}" \ > coverage.lcov - name: Upload coverage to Codecov From e8722adc301cd0aa3d6db251b41e6d7de84a327a Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Mon, 14 Sep 2026 21:12:25 +0900 Subject: [PATCH 4/6] Point the canvas golden workaround at #49 --- CLAUDE.md | 2 +- CONTRIBUTING.md | 3 ++- Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 86eee99..251ca99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,7 +91,7 @@ SVG tests avoid raw string literals (`#"..."#`) when the expected string contain **Drawing-scenario golden tests.** `Tests/TortoiseTestSupport/` (a non-product target) defines `DrawingScenario.all` — feature-grouped tortoise programs that together cover every `TortoiseCommand` case. Both renderers are verified against the same scenarios via pointfreeco/swift-snapshot-testing: - `TortoiseSVGTests/DrawingScenarioSVGTests` compares full SVG strings against goldens in `Tests/TortoiseSVGTests/__Snapshots__/`. -- `TortoiseUITests/DrawingScenarioCanvasTests` (macOS-only, `#if os(macOS)`) renders `TortoiseCanvas` via `ImageRenderer` (`scale = 2`, instant mode forced by prepending `speed = 0`) and compares PNGs in `Tests/TortoiseUITests/__Snapshots__/` with `.canvasGolden` (byte-wise `precision: 0.995`, defined in `CanvasGoldenSnapshotting.swift`) to absorb OS-level antialiasing drift. **Do not add `perceptualPrecision`**: swift-snapshot-testing's perceptual path passes a bare `CGRect` to `CIAreaAverage`, which Core Image on macOS 27 rejects with an uncaught exception that kills the whole test process. It only triggers when bytes differ, so it passes on the recording macOS and crashes on the CI runner's. +- `TortoiseUITests/DrawingScenarioCanvasTests` (macOS-only, `#if os(macOS)`) renders `TortoiseCanvas` via `ImageRenderer` (`scale = 2`, instant mode forced by prepending `speed = 0`) and compares PNGs in `Tests/TortoiseUITests/__Snapshots__/` with `.canvasGolden` (byte-wise `precision: 0.995`, defined in `CanvasGoldenSnapshotting.swift`) to absorb OS-level antialiasing drift. **Do not add `perceptualPrecision`**: swift-snapshot-testing's perceptual path passes a bare `CGRect` to `CIAreaAverage`, which Core Image on macOS 27 rejects with an uncaught exception that kills the whole test process. It only triggers when bytes differ, so it passes on the recording macOS and crashes on the CI runner's. Tracked in #49 (seen on macOS 27 / Xcode 27 betas; to be re-checked on the release). After intentionally changing scenario programs or renderer output, re-record **both** golden sets in the same commit and visually inspect them: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa9cc44..71ff472 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,8 @@ The PNG goldens depend on OS-level rendering. The current set was recorded on byte-wise tolerance (`precision: 0.995`, defined once as `.canvasGolden` in `Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift`) absorbing antialiasing drift between OS versions. Don't add `perceptualPrecision`: that -comparison crashes the test process on macOS 27. If a PR bumps the CI runner image +comparison crashes the test process on macOS 27 +([#49](https://github.com/temoki/TortoiseGraphics2/issues/49)). If a PR bumps the CI runner image (`runs-on:`) or you re-record on a different macOS version, confirm the PNG golden tests still pass in CI — and if they don't, re-record both golden sets and visually inspect them as described above. diff --git a/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift b/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift index 5b47c34..859b3b4 100644 --- a/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift +++ b/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift @@ -12,6 +12,9 @@ /// exception — crashing the whole test process rather than failing one /// test. An exact match never reaches that path, which is why the goldens /// still pass on the macOS they were recorded on. + /// + /// Seen on macOS 27 / Xcode 27 betas; whether the release fixes it, and so + /// whether `perceptualPrecision` can come back, is tracked in #49. static var canvasGolden: Snapshotting { .image(precision: 0.995) } From a44a90349232fb61efc88a108733094c5ad55787 Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Sat, 19 Sep 2026 10:53:15 +0900 Subject: [PATCH 5/6] Use the official swift:6.4 image for the Linux job The 6.4 release-branch nightly was a stand-in until Docker Hub published swift:6.4, which it now has. --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12dee7f..ed8f6e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,9 +82,7 @@ jobs: linux: name: Linux (Core + SVG) runs-on: ubuntu-latest - # TODO: switch to the official `swift:6.4` image once it is published; - # until then, the 6.4 release-branch nightly is the only 6.4 toolchain. - container: swiftlang/swift:nightly-6.4.x-noble + container: swift:6.4 steps: - uses: actions/checkout@v7 From 5ee64b62e02353ceb988a4387b3aca150e7d691c Mon Sep 17 00:00:00 2001 From: Tomoki Kobayashi Date: Sat, 19 Sep 2026 11:21:30 +0900 Subject: [PATCH 6/6] Record that the #49 crash survives Xcode 27 GA --- CLAUDE.md | 2 +- Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 251ca99..e04e660 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,7 +91,7 @@ SVG tests avoid raw string literals (`#"..."#`) when the expected string contain **Drawing-scenario golden tests.** `Tests/TortoiseTestSupport/` (a non-product target) defines `DrawingScenario.all` — feature-grouped tortoise programs that together cover every `TortoiseCommand` case. Both renderers are verified against the same scenarios via pointfreeco/swift-snapshot-testing: - `TortoiseSVGTests/DrawingScenarioSVGTests` compares full SVG strings against goldens in `Tests/TortoiseSVGTests/__Snapshots__/`. -- `TortoiseUITests/DrawingScenarioCanvasTests` (macOS-only, `#if os(macOS)`) renders `TortoiseCanvas` via `ImageRenderer` (`scale = 2`, instant mode forced by prepending `speed = 0`) and compares PNGs in `Tests/TortoiseUITests/__Snapshots__/` with `.canvasGolden` (byte-wise `precision: 0.995`, defined in `CanvasGoldenSnapshotting.swift`) to absorb OS-level antialiasing drift. **Do not add `perceptualPrecision`**: swift-snapshot-testing's perceptual path passes a bare `CGRect` to `CIAreaAverage`, which Core Image on macOS 27 rejects with an uncaught exception that kills the whole test process. It only triggers when bytes differ, so it passes on the recording macOS and crashes on the CI runner's. Tracked in #49 (seen on macOS 27 / Xcode 27 betas; to be re-checked on the release). +- `TortoiseUITests/DrawingScenarioCanvasTests` (macOS-only, `#if os(macOS)`) renders `TortoiseCanvas` via `ImageRenderer` (`scale = 2`, instant mode forced by prepending `speed = 0`) and compares PNGs in `Tests/TortoiseUITests/__Snapshots__/` with `.canvasGolden` (byte-wise `precision: 0.995`, defined in `CanvasGoldenSnapshotting.swift`) to absorb OS-level antialiasing drift. **Do not add `perceptualPrecision`**: swift-snapshot-testing's perceptual path passes a bare `CGRect` to `CIAreaAverage`, which Core Image on macOS 27 rejects with an uncaught exception that kills the whole test process. It only triggers when bytes differ, so it passes on the recording macOS and crashes on the CI runner's. Tracked in #49 (still reproduces on Xcode 27.0 GA; re-check once the runner image ships a GA macOS 27). After intentionally changing scenario programs or renderer output, re-record **both** golden sets in the same commit and visually inspect them: diff --git a/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift b/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift index 859b3b4..c88c2dc 100644 --- a/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift +++ b/Tests/TortoiseUITests/CanvasGoldenSnapshotting.swift @@ -13,8 +13,9 @@ /// test. An exact match never reaches that path, which is why the goldens /// still pass on the macOS they were recorded on. /// - /// Seen on macOS 27 / Xcode 27 betas; whether the release fixes it, and so - /// whether `perceptualPrecision` can come back, is tracked in #49. + /// Still reproduces on Xcode 27.0 GA (27A266a); the CI runner's macOS 27 is + /// itself still a beta build, so whether `perceptualPrecision` can ever come + /// back is tracked in #49. static var canvasGolden: Snapshotting { .image(precision: 0.995) }