Integration: keep the ATN contract tests, drop the extra workflow (#7561)

The contract tests no longer compiled on main (AtnItem::dictionary is now
Vec<u8>, so it is compared with b"Future event"), sat outside main's
tests/it layout, and the new atn.yml added six legs (two macOS) to every
PR against ci.yml's policy. Move the tests into tests/it so the existing
CI runs them, drop atn.yml and docs/atn-testing.md, keep main's ci.yml and
fuzz target, and note the committed fixtures in ATTRIBUTION.md.

Co-authored-by: denstu473 <63150090+denstu473@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Mann
2026-11-01 14:25:51 +01:00
parent 893d12bee2
commit 4b053e15ae
9 changed files with 24 additions and 282 deletions
-166
View File
@@ -1,166 +0,0 @@
name: ATN
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
merge_group:
schedule:
- cron: "17 4 * * *"
workflow_dispatch:
inputs:
fuzz_seconds:
description: "Seconds of ATN parser fuzzing"
type: choice
options: ["60", "300", "600"]
default: "60"
permissions:
contents: read
concurrency:
group: atn-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_RELEASE_DEBUG: "0"
# Keep optimized arithmetic behavior without paying for release LTO in a parser test job.
CARGO_PROFILE_RELEASE_LTO: "false"
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16"
jobs:
parser:
name: ATN parser (${{ matrix.target }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
# These small CPU-only jobs can share the macOS pool with release builds.
max-parallel: 3
matrix:
include:
- { os: ubuntu-24.04, target: x86_64-unknown-linux-gnu, arch: X64 }
- { os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, arch: ARM64 }
- { os: windows-2025, target: x86_64-pc-windows-msvc, arch: X64 }
- { os: windows-11-arm, target: aarch64-pc-windows-msvc, arch: ARM64 }
- { os: macos-15-intel, target: x86_64-apple-darwin, arch: X64 }
- { os: macos-15, target: aarch64-apple-darwin, arch: ARM64 }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: atn-parser-${{ matrix.target }}
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-on-failure: true
- name: Record native execution environment
env:
EXPECTED_ARCH: ${{ matrix.arch }}
TEST_TARGET: ${{ matrix.target }}
run: |
mkdir -p atn-results
# A successful cross-compilation must never stand in for native execution.
test "$RUNNER_ARCH" = "$EXPECTED_ARCH"
{
git rev-parse HEAD
printf 'runner=%s arch=%s target=%s\n' "$RUNNER_OS" "$RUNNER_ARCH" "$TEST_TARGET"
printf 'image=%s version=%s\n' "${ImageOS:-unknown}" "${ImageVersion:-unknown}"
rustc -vV
cargo -V
} | tee atn-results/environment.txt
- name: Parser unit tests
env:
TEST_TARGET: ${{ matrix.target }}
run: |
set -o pipefail
cargo test --locked -p photocraft-psd --target "$TEST_TARGET" --lib atn::tests:: -- --nocapture 2>&1 | tee atn-results/unit.log
- name: Parser contract tests (debug)
env:
TEST_TARGET: ${{ matrix.target }}
run: |
set -o pipefail
cargo test --locked -p photocraft-psd --target "$TEST_TARGET" --test atn_contract -- --nocapture 2>&1 | tee atn-results/debug.log
- name: Parser contract tests (release)
env:
TEST_TARGET: ${{ matrix.target }}
run: |
set -o pipefail
cargo test --locked --release -p photocraft-psd --target "$TEST_TARGET" --test atn_contract -- --nocapture 2>&1 | tee atn-results/release.log
- name: Upload parser evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: atn-parser-${{ matrix.target }}
path: atn-results/
if-no-files-found: error
retention-days: 14
parser-gate:
name: ATN parser gate
needs: parser
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require every native parser leg
env:
PARSER_RESULT: ${{ needs.parser.result }}
run: test "$PARSER_RESULT" = success
fuzz:
name: ATN parser fuzz
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
key: atn-fuzz
workspaces: crates/psd/fuzz
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-on-failure: true
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked
- name: Seed parser fuzzing with the contract fixtures
run: |
mkdir -p crates/psd/fuzz/corpus/atn atn-results
cp crates/psd/tests/fixtures/atn/*.atn crates/psd/fuzz/corpus/atn/
{
git rev-parse HEAD
rustc -vV
cargo fuzz --version
sha256sum crates/psd/tests/fixtures/atn/*.atn
} | tee atn-results/fuzz-environment.txt
- name: Fuzz ATN parsing and writing
working-directory: crates/psd
env:
FUZZ_SECONDS: ${{ inputs.fuzz_seconds || '60' }}
run: |
set -o pipefail
cargo fuzz run atn -- -max_total_time="$FUZZ_SECONDS" -max_len=65536 -rss_limit_mb=512 -timeout=5 2>&1 | tee ../../atn-results/fuzz.log
- name: Upload fuzz evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: atn-fuzz-evidence
path: |
atn-results/
crates/psd/fuzz/artifacts/atn/
crates/psd/fuzz/corpus/atn/
if-no-files-found: warn
retention-days: 14
+4 -3
View File
@@ -120,7 +120,8 @@ Artwork shown in the screenshots (all public domain, via Wikimedia Commons; deta
## Test data (not committed, not shipped)
`corpus/` is gitignored and no test fixtures are committed to the repository.
`corpus/` is gitignored and no test fixtures are committed to the repository, except the small
synthetic parser fixtures listed under *Synthetic parser test fixtures* below.
`cargo xtask corpus --all` fetches every corpus at the pinned commits in `xtask/src/corpus_pins.rs`
and verifies each file against the sha256 lists in `xtask/*.sha256`, with the upstream licence
next to the files:
@@ -138,14 +139,14 @@ next to the files:
| `corpus/pngsuite/` | PngSuite | Willem van Schaik | <http://www.schaik.com/pngsuite/> | Public domain |
| `corpus/pixls/` (11 files) | Real camera raws, one per decode path: Canon PowerShot SX50 HS (CR2 and its DNG conversion), Nikon D3 uncompressed NEF and Z6 paired 12/14-bit lossy and packed NEFs, Sony DSC-RX0 cRAW ARW, Panasonic DC-G9 high-res RW2, Olympus E-1 GRBG ORF, plus the E-5 packed ORF as the documented known-unsupported case. Per file (each CC0, per its database entry): [IMG_4059.CR2](https://raw.pixls.us/data/Canon/PowerShot%20SX50%20HS/IMG_4059.CR2), [CRW_4061.DNG](https://raw.pixls.us/data/Canon/PowerShot%20SX50%20HS/CRW_4061.DNG), [JD1_8203.NEF](https://raw.pixls.us/data/Nikon/D3/JD1_8203.NEF), [DSC00009.ARW](https://raw.pixls.us/data/Sony/DSC-RX0/DSC00009.ARW), [P1000475.RW2](https://raw.pixls.us/data/Panasonic/DC-G9/P1000475.RW2), [E_1__C106743_gredos.ORF](https://raw.pixls.us/data/Olympus/E-1/E_1__C106743_gredos.ORF), [_7061961_copy.ORF](https://raw.pixls.us/data/Olympus/E-5/_7061961_copy.ORF), [DSC_0750.NEF](https://raw.pixls.us/data/Nikon/Z%206/DSC_0750.NEF), [DSC_0751.NEF](https://raw.pixls.us/data/Nikon/Z%206/DSC_0751.NEF), [DSC_0753.NEF](https://raw.pixls.us/data/Nikon/Z%206/DSC_0753.NEF), [DSC_0754.NEF](https://raw.pixls.us/data/Nikon/Z%206/DSC_0754.NEF) | The photographers of the pixls.us raw samples database | [raw.pixls.us](https://raw.pixls.us) `data/`, fetched per [xtask/src/corpus_pins.rs](xtask/src/corpus_pins.rs) | CC0 / public domain, per file as linked |
Files copied into `corpus/` by hand (tiff, exr, raw) must be MIT, BSD or CC0.
## Synthetic parser test fixtures
| Path | Title | Author | Source | License |
|---|---|---|---|---|
| `crates/psd/tests/fixtures/atn/*.atn` | Independently encoded ATN version-16 parser fixtures | PhotoCraft contributors | Original synthetic test data; wire layout documented in `crates/psd/src/atn.rs`; provenance in the adjacent README | MIT OR Apache-2.0 |
Files copied into `corpus/` by hand (tiff, exr, raw) must be MIT, BSD or CC0.
## Embedded-vector editor source
The external SVG watcher, engine/UI handoff and bounded SVG rendering in this change
+7 -5
View File
@@ -1,12 +1,14 @@
//! Fuzz the `.atn` actions parser: any input must return `Ok` or `Err`, never panic, and
//! anything that parses must survive writing, reparsing and stable canonical writing.
//! anything that parses must survive a write and parse back equal.
#![no_main]
use libfuzzer_sys::fuzz_target;
#[path = "../../tests/common/atn_roundtrip.rs"]
mod atn_roundtrip;
use photocraft_psd::atn;
fuzz_target!(|data: &[u8]| {
atn_roundtrip::check(data);
if let Ok(f) = atn::parse(data) {
let bytes = atn::write(&f).expect("a parsed set is writable");
let again = atn::parse(&bytes).expect("a written set parses");
assert_eq!(again.actions, f.actions);
}
});
+2 -2
View File
@@ -13,7 +13,7 @@ oracles.
| `descriptors.atn` | 859 | Every descriptor value variant and every reference form currently represented by the parser | `6c76502822c1f36c8060a71adb227c4540663518785bbab1b70951dac0ef58ad` |
| `float-bits.atn` | 428 | NaN payload, negative zero, infinities, and floats inside lists, objects and arrays | `0aa6278a569c777f249b305e889896132c2a87e44a24cb1a55d1d3fc0928fd0c` |
`tests/atn_contract.rs` checks the independently encoded bytes and expected public structures.
The ATN fuzz job copies the same files into its seed corpus. A fixture change should explain
`tests/it/atn_contract.rs` checks the independently encoded bytes and expected public structures.
A fixture change should explain
the changed wire fields and update this table; do not regenerate expectations merely to make a
failing parser pass.
@@ -1,4 +1,4 @@
//! Portable public-API contract, executed natively in debug and release by ATN CI.
//! Public-API contract for the `.atn` parser, against independently encoded wire fixtures.
use photocraft_psd::PsdError;
use photocraft_psd::atn::{self, AtnAction, AtnEvent, AtnFile, AtnItem};
use photocraft_psd::descriptor::{Class, Descriptor, Id, MAX_DEPTH, ObjectArray, ReferenceItem, UnicodeString, Value};
@@ -7,10 +7,10 @@ use photocraft_psd::descriptor::{Class, Descriptor, Id, MAX_DEPTH, ObjectArray,
mod atn_roundtrip;
const FIXTURES: &[(&str, &[u8])] = &[
("empty", include_bytes!("fixtures/atn/empty.atn")),
("metadata", include_bytes!("fixtures/atn/metadata.atn")),
("descriptors", include_bytes!("fixtures/atn/descriptors.atn")),
("float bits", include_bytes!("fixtures/atn/float-bits.atn")),
("empty", include_bytes!("../fixtures/atn/empty.atn")),
("metadata", include_bytes!("../fixtures/atn/metadata.atn")),
("descriptors", include_bytes!("../fixtures/atn/descriptors.atn")),
("float bits", include_bytes!("../fixtures/atn/float-bits.atn")),
];
fn file_with(desc: Descriptor) -> AtnFile {
@@ -55,7 +55,7 @@ fn unicode_metadata_unknown_events_and_dialog_flags_are_preserved() {
let future = &action.items[1];
assert_eq!(future.event, AtnEvent::Name("syntheticFutureEvent".into()));
assert_eq!((future.expanded, future.enabled, future.with_dialog, future.dialog_options), (false, false, true, 255));
assert_eq!(future.dictionary, "Future event");
assert_eq!(future.dictionary, b"Future event");
assert!(future.descriptor.is_none());
assert_eq!(file.actions[1].name, UnicodeString::new_nul("Empty"));
assert!(file.actions[1].items.is_empty());
@@ -1,4 +1,4 @@
//! Shared invariant for the parser contract suite and the actual cargo-fuzz target.
//! Shared round-trip invariant for the `.atn` parser contract suite.
use photocraft_psd::atn::{self, AtnFile};
use photocraft_psd::descriptor::{Descriptor, Value};
+1
View File
@@ -1,5 +1,6 @@
//! The crate's integration tests, linked into one test binary.
mod atn_contract;
mod builder;
mod corpus;
mod corpus_atn;
-97
View File
@@ -1,97 +0,0 @@
# ATN parser tests and CI
`.github/workflows/atn.yml` adds a focused parser check on every pull request, merge-group run,
main push, nightly run and manual dispatch. It runs even on unrelated pull requests so its
stable `ATN parser gate` check is always available for branch protection. Making that check
required is a repository setting; adding the workflow does not change branch protection.
## Native platform matrix
| OS image | Executed target |
|---|---|
| Ubuntu 24.04 x64 | `x86_64-unknown-linux-gnu` |
| Ubuntu 24.04 ARM64 | `aarch64-unknown-linux-gnu` |
| Windows Server 2025 x64 | `x86_64-pc-windows-msvc` |
| Windows 11 ARM64 | `aarch64-pc-windows-msvc` |
| macOS 15 Intel | `x86_64-apple-darwin` |
| macOS 15 Apple Silicon | `aarch64-apple-darwin` |
Runner labels are listed in GitHub's
[hosted-runner reference](https://docs.github.com/en/actions/reference/runners/github-hosted-runners).
Every leg verifies the runner architecture, runs the parser unit tests, and executes the public
parser contract suite in debug and release. These are native test executions, not cross-compile
checks. The release check disables LTO to keep CI small while retaining optimized arithmetic
behavior. At most three matrix legs run concurrently. Full workspace CI keeps its existing
platform selection; this separate job builds only the standalone PSD crate and its dependencies.
The matrix tests CPU parsing and writing. It does not certify graphics drivers, GPU previews,
UI cancellation, ATN command conversion/playback, WebAssembly execution or Photoshop parity.
Those need their own execution and expected-result checks. Processor architecture coverage is
not an exhaustive inventory of CPU models or operating-system versions.
## Contract and fixtures
`crates/psd/tests/atn_contract.rs` exercises the public `photocraft_psd::atn::{parse, write}` APIs.
Its four committed synthetic fixtures are independently encoded, not generated by the writer
under test. The checks cover:
- Byte-exact parsing/writing, explicit endianness, names, action/step order and empty actions.
- UTF-16 units, including supplementary characters and unpaired surrogates.
- Function keys, modifiers, panel state, enabled state, dialog flags and unknown event names.
- Every descriptor value variant and reference form, including opaque alias/path/raw bytes.
- Exact floating-point bits, including NaN payloads, signed zero and infinities.
- Every truncated fixture, invalid versions/tags, declared action/step limits and excessive
descriptor nesting, with passing valid controls.
- Deterministic generated metadata/float cases and seed mutations through the same invariant
called by the real fuzz target.
The parser preserves file representation; native engine option semantics remain the
responsibility of `crates/engine/src/atn_cmds/tests.rs` and future playback regression suites.
Run the focused checks from the repository root:
```sh
cargo test --locked -p photocraft-psd --lib atn::tests::
cargo test --locked -p photocraft-psd --test atn_contract
cargo test --locked --release -p photocraft-psd --test atn_contract
```
Fixtures and their provenance/checksums are documented in
`crates/psd/tests/fixtures/atn/README.md`. New Photoshop-authored interoperability oracles belong
in the separately pinned `photocraft-corpus` repository, following the existing corpus policy.
## Fuzzing
The dedicated ATN workflow runs the existing `psd/atn` cargo-fuzz target on Linux nightly and on
manual dispatch. It starts with the four valid fixtures, runs for 60 seconds nightly, and caps
input length at 64 KiB, resident memory at 512 MiB and a single input at five seconds. Manual
runs can select 60, 300 or 600 seconds. The general CI workflow's existing manual fuzz matrix
also includes `psd/atn`.
The shared invariant in `tests/common/atn_roundtrip.rs` requires every successfully parsed set
to write and reparse with the same set/action metadata, descriptor structure and floating-point
bits. NaNs are compared by bits before normalization for AST equality, avoiding false failures
from `NaN != NaN`. Only trailing-input diagnostic warnings are excluded from serialized state.
Canonical output must remain byte-stable on another write.
```sh
mkdir -p crates/psd/fuzz/corpus/atn
cp crates/psd/tests/fixtures/atn/*.atn crates/psd/fuzz/corpus/atn/
cd crates/psd
cargo +nightly fuzz run atn -- -max_total_time=60 -max_len=65536 -rss_limit_mb=512 -timeout=5
```
Run cargo-fuzz where its sanitizer/runtime are supported; the deterministic contract suite runs
on every matrix platform. Every real failure gets a minimized committed regression fixture and
an expected behavior assertion. Do not hide a failure by adding an ignore or weakening a check.
## Evidence and scope
Parser legs upload the exact commit, runner/image, architecture, Rust/Cargo versions, and test
logs, including failures. Fuzz runs upload environment details, fixture checksums, the evolved
corpus, logs and crash artifacts. Evidence is retained for 14 days. A failed, cancelled or
skipped parser matrix prevents the aggregate gate from succeeding.
Local validation on one platform does not establish that hosted jobs have passed. Confirm all
six native legs in GitHub Actions before describing the change as verified on that matrix.
+3 -2
View File
@@ -631,8 +631,9 @@ PhotoCraft equivalent.
Events outside the table load as `actions.photoshopStep` steps that keep the descriptor
verbatim (so Save Actions writes them back) and fail at playback with the step's Photoshop name.
The focused native parser matrix, public-API contract fixtures, scheduled fuzzing and local
commands are described in [ATN parser tests and CI](atn-testing.md).
`crates/psd/tests/it/atn_contract.rs` checks the parser's public API against four small,
independently encoded fixtures in `crates/psd/tests/fixtures/atn/` (provenance and checksums in
its README): `cargo test -p photocraft-psd --test it atn_contract::`.
## Remove Tool quality