Files
effectcraft/docs/testing.md
T
13470ca161 Add affected CI and replayable crash repros (#1224)
* AG4: xtask ci --affected

cargo xtask ci --affected [--base REV] [--dry-run] runs fmt, clippy, tests (nextest + doctests when available), layers, assets (only on asset changes) and wasm (only for affected L0-L4/web crates) for the packages a change can affect: changed files map to the owning package and to packages whose sources read them via ../ paths, expanded to reverse dependents. Workspace-wide inputs (root Cargo.toml, Cargo.lock, .cargo/, xtask/, toolchain, clippy.toml) fall back to full CI. Documented in docs/testing.md, AGENTS.md and CLAUDE.md.

(cherry picked from commit f3ca84fc2ee3948582cbc94e4c99ee510e829540)

(cherry picked from commit 8a2cdcadf447eafa57ab14016beae4420f002af0)
(cherry picked from commit d8a4301bc1)

(cherry picked from commit 36402c61ac2f7e29ecd76a6cfc89365cc2572e0a)

* AO1: fast CI profile, looser timing tests

- [profile.ci] (release without LTO, 16 codegen units); `xtask ci` runs clippy
  and tests with --profile ci instead of --release.
- Wall-clock asserts in engine memory_tick, gpu OpenGL decline and script
  socket tests keep their intent with bounds that survive a loaded machine.
- docs/testing.md and CONTRIBUTING.md mention the profile.

(cherry picked from commit b45c8a379351ebdfda0d7076ad4b740baf6f2087)

(cherry picked from commit fa1ea6a107c4c1936cde89b518a05adc73f2b4b0)
(cherry picked from commit e5d5ed80e9)

(cherry picked from commit d2c67660c70764f9c56880a642ad7f0d004a7f92)

* AG6: journal replay repros

Bug reports become replay scripts and crash regression tests become data files:
- engine::replay: a JSON-lines repro format (start line, command steps with
  expect_error, expect queries with JSON pointer + equals, expect_no_error),
  a never-panicking runner that reports per-step ok/error and the first failure,
  and journal export (from the last project open; saves left out; size-capped).
- journal.export {path?} and help.exportRepro {path?} (not journaled).
- Crash capture: when the last-resort guard catches a panic in the outermost
  command, the journal plus that command is saved to Repros/crash-*.jsonl in the
  settings store (newest 20 kept) and the error names the file.
- Commands run by a journaled command (script steps) are no longer journaled twice.
- effectcraft-cli replay SCRIPT.jsonl [--json] [--keep-going]: exits 1 on a failed
  step, unmet expectation or panic.
- crates/host/tests/repros.rs replays every tests/repros/*.jsonl; three samples
  from past crash fixes (zero frame rate, Layer Settings with no selection,
  New Comp from Selection with hostile overlap).
- Docs: docs/agents.md "Repro scripts"; AGENTS.md: crash fix regression test =
  a .jsonl in tests/repros.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

(cherry picked from commit c38509d7dafe507856c48c78db65e00b94b0cebc)

(cherry picked from commit e55b56dbf76716adfcbfe4cc773df19a74edc419)
(cherry picked from commit 96a63855a9)

(cherry picked from commit c59351ab3bd697f0868e41230dd883960d94a540)

* xtask: ci and ci --affected run cargo test and nextest with --no-fail-fast

One run now lists every failing crate instead of stopping at the first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qXL3GPNnY9BSFF2XoU2Yu

(cherry picked from commit e46c7debe738eca62b35bf7392ab77637f6a33ae)

* Agents: confinement write-checks journal.export, help.exportRepro and file.exportTimeline

Review on #859 (split into this PR): their output paths were only read-checked (relative ones
not at all). Test: confinement_covers_every_command_that_writes_a_file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014d65YdNxQmjRkbXRtP2NwH

* #1224: handle bin-only CI targets and BOM repros without weakening timing tests

* #1224: run the full gate when nextest policy changes

---------

Co-authored-by: Elias <hakifx91@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 14:57:32 +01:00

13 KiB
Raw Blame History

Testing

cargo test --workspace     # everything
cargo xtask ci             # fmt, clippy -D warnings, tests, pdf-deps, lock-patches, layers, assets, wasm
cargo xtask ci --affected  # the same gates, scoped to crates your change can affect

The After Effects test projects of crates/aep are not in the repository; without them the crates/aep, engine and ui-egui tests that need one print SKIPPED: fixture <name> missing and pass. They live in storytold/effectcraft-corpus. cargo xtask aep-corpus fetches them at the commit pinned in crates/aep/tests/fixtures/corpus.lock, checks every file against the SHA-256 listed there (any mismatch fails and installs nothing) and installs them in corpus/aep. Point EFFECTCRAFT_AEP_FIXTURES at that folder to run those tests too, and set EFFECTCRAFT_AEP_FIXTURES_REQUIRED=1 as well to make a missing project fail its test instead of skipping (a corpus run that must not pass by skipping):

cargo xtask aep-corpus
EFFECTCRAFT_AEP_FIXTURES=corpus/aep EFFECTCRAFT_AEP_FIXTURES_REQUIRED=1 cargo test -p effectcraft-aep

The dev profile keeps debug info to line tables (fast builds, readable backtraces). To debug interactively — local variables, parameters, column breakpoints — build with the dev-full profile: cargo run --profile dev-full -p effectcraft -- --demo (see CONTRIBUTING.md).

cargo xtask ci builds in the dev profile and runs the tests with cargo nextest run (plus cargo test --doc) when cargo-nextest is installed, and with cargo test otherwise. It then runs missing_translations_report with --nocapture, which prints the interface strings each language has no translation for. Those strings show in English; the report always passes.

Each crate with several integration-test files has one test binary, tests/it/main.rs, whose modules are the files in tests/it/. Run one file with cargo test -p <crate> --test it <module>:: or cargo nextest run -p <crate> -E 'binary(it) and test(/^<module>::/)'.

Affected-only CI

Use cargo xtask ci --affected to run the gates for the crates a change can affect. The full cargo xtask ci remains the required check before a release and when affected mode falls back because a workspace-wide input changed.

cargo xtask ci --affected                  # base: the merge-base of HEAD with main
cargo xtask ci --affected --base HEAD~3    # any git revision
cargo xtask ci --affected --dry-run        # print the plan only

It collects files that differ from the base (committed, staged, unstaged and untracked), preserving Unicode and whitespace in their names. Each file maps to the workspace package whose directory holds it and every package whose sources point at it with a ../ literal, such as include_bytes!("../../../assets/fonts/Inter-Regular.ttf") or concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/preferences.md"). Deleted and renamed inputs still select their readers, including when the input's entire directory no longer exists.

The affected set is those packages plus everything that depends on them (transitively through normal and build dependencies) and every package that uses one of them as a dev-dependency. Clippy and tests use the same dev profile as full CI. Both modes use the same test runner: nextest plus doctests when installed, otherwise cargo test plus doctests. GPU tests run in one libtest process, and the translation report reruns with visible output when ui-egui is affected. The audio-backend probe runs before building affected crates.

Formatting, PDF dependency policy, lock-patch and layer checks always run. Assets are checked when an asset, an .attribution sidecar or ATTRIBUTION.md changes. The wasm check selects affected L0–L4 and web crates. Every step runs even when an earlier step fails; the final result names failed steps and includes timings.

Changes to the root Cargo.toml, Cargo.lock, .cargo/, xtask/, rust-toolchain(.toml), clippy.toml or .config/nextest.toml can change every crate's build or test policy, so they run the full CI instead and say why. Changes no crate reads (most of docs/, README.md, .github/, packaging/) only get the cheap checks.

Limits: a crate that reads a repo file at run time through a path that is not a ../ literal in its sources is not detected, and building a subset of packages can unify features differently from the whole workspace. Non-UTF-8 Git paths stop the command rather than silently omitting checks. Run the full cargo xtask ci before a release.

What the tests cover

  • Engine commands (crates/engine/src/tests*.rs): every command family, including undo and redo, and that every menu entry resolves to a registered command.
  • Rendering (crates/render): pixel checks on small compositions, for blend modes, masks, mattes, time remapping, 3D projection, lighting and shadows. Fast paths (the layer cache, the affine compositor, blurs, motion blur accumulation) are checked against their reference implementations.
  • Effects (crates/effects): each effect has a determinism test and at least one behaviour test (identity at neutral settings, known pixel results, or simulations giving the same frame whether you seek straight to it or play up to it). A registry-wide test checks that effects which read the clock are declared time-dependent. crates/effects/tests/it/sim_golden.rs pins exact pixel hashes of the simulation effects the GPU shares plans with (CC Rainfall … Card Wipe) at several settings, times, bit depths and resolutions, so any change to their CPU output fails (re-pin with SIM_GOLDEN_PRINT=1 after an intended change; pinned for aarch64 macOS, with the hashes another macOS release's libm gives for unchanged code in LIBM_VARIANTS).
  • Export (crates/export/tests): every format is encoded and decoded back, checking frame count, size and pixels. When ffmpeg/ffprobe are installed they are used as an outside check; they are never linked or shipped.
  • Automation (crates/automation, apps/effectcraft-cli/tests): MCP protocol round trips and the command-line tool's JSON output.
  • Interface (crates/ui-egui): headless egui_kittest tests for panels and dialogs.

Looking at the interface without a window

cargo run -p effectcraft-ui-egui --example snapshot -- --out ui.png \
  --step '{"method":"engine.execute","params":{"command":"layer.select","params":{"layers":["#2"]}}}'

Each --step is a control-channel request; {"method":"snap","params":{"path":"x.png"}} writes an intermediate image.

Benchmarks

cargo run --release -p effectcraft-cli -- bench --n 10 --play 30

prints per-layer and per-effect timings for one frame, and playback timings with and without the layer cache.

Render benchmark (cargo xtask bench)

nice -n 10 cargo xtask bench --bin target/release/effectcraft-cli   # or no --bin: one `--release` build into target/bench
nice -n 10 cargo xtask bench --quick --compare before.jsonl        # 8 frames instead of 60, % change against an earlier run
cargo xtask bench --project F.ecproj --comp "Main"                 # any project's comp

measures memory, CPU and wall time of whole effectcraft-cli runs, so every optimisation has a before and after number. It generates four deterministic scenarios into target/bench/projects with engine commands (no committed media): demo (the built-in demo comp), layers (1080p, 100 solid, text and shape layers with Gaussian Blur, Glow, Levels, Curves, Drop Shadow and keyframes), precomps (static and animated precomps nested three deep, each used several times) and footage (a rendered PNG sequence and H.264 clip on several 1080p layers, plus a 4K still). Each is opened (open), rendered at Full and Half (PNG sequence, deleted afterwards) and as one frame. /usr/bin/time -l on macOS gives peak memory footprint, max RSS, user + sys CPU and instructions retired; /usr/bin/time -v on Linux gives max RSS and CPU; elsewhere wall time only.

It prints a table and appends one JSON line per run to target/bench/results.jsonl (git rev, date, load average, scenario, settings, metrics); --compare OLD.jsonl prints the % change against the latest matching runs there, --compare OLD --with NEW compares two files without rendering. --repeat N runs every scenario N times; with N > 1, one "kind": "summary" line per key follows the runs. Its metrics are the medians of the successful runs (byte and instruction counts rounded), summary holds runs, failed and median/p90/min/max per metric (p90 is the nearest rank, so it equals the max for N < 10), and its other fields (date, load, ...) are those of the key's last run; scripts that average runs should skip kind: summary lines. One failed run marks the summary failed. Comparisons use the latest line per key, so a key is a median only if its summary is the latest line (a later single run, or a batch that aborted before its summaries, leaves a raw run latest). The table labels each side median of N or 1 run and warns when they differ; --fail-over refuses such keys. The table notes the 1-minute load average: on a busy machine wall time is noise, so compare peak memory and instructions retired, and rerun when the load differs a lot.

Results files allow blank lines; corrupt records or invalid comparison fields report the file and line instead of being discarded. --fail-over PCT requires the same run keys on both sides, successful latest runs, and at least one comparable resource metric per run. Failed latest runs, missing coverage, empty results and wall-time-only measurements cannot pass the gate. Omit --fail-over for an exploratory partial comparison; failed latest runs remain visible.

Unexpected desktop exit diagnostics

The desktop starts a separate, lightweight reporter before loading fonts, projects or the GPU. A normal exit (including a handled startup error) sends a clean marker only after desktop teardown. macOS finishes at the end of its successful exit hook because its native event loop terminates without returning to main; other platforms finish after the event loop returns. A teardown panic leaves reporting armed. An abort or other unexpected process termination closes the pipe without that marker. The helper saves a local report under the session log folder's Crash Reports directory and shows a native error dialog. Control-server launches save the report without a modal dialog. Reports contain version/platform, UTC detection and observer-start times, observed duration, GUI uptime at the last target-present sample, and at most the last 64 KiB of the matching session's log. A separate ring retains the last 32 timestamped, fixed-vocabulary backend and GPU observations even if startup messages have fallen out of the log tail. WGPU_BACKEND values are normalized to known backend names; arbitrary environment text is not copied. Logging filters and the session file size cap can omit later events from the log tail. They are never uploaded automatically. Review logs for private project paths before sharing.

On Windows the helper acquires a process handle before acknowledging readiness and retains it until the GUI exits. It records the real OS exit status in unsigned hexadecimal, with a small lookup of known status names. These bits are not a verified exception record: software can explicitly set the same code, and an access violation does not identify the faulting module or prove a driver crash. Other platforms report exit status as unavailable. A lost pipe while the GUI is still present, or a failed/absent process refresh, is not reported as confirmed termination.

The helper samples only the target process and system RAM at roughly one-second intervals, retaining a count, last-attempt time, sampled resident-memory maximum, Windows private-commit maximum (virtual memory on other platforms), and system available-RAM minimum. This is a fixed-size summary, not a memory dump. OS query failure can leave stale or zero values, and sampling misses short spikes. System commit limits/usage and GPU VRAM budgets are not captured. No threshold is treated as proof of memory exhaustion. Typed wgpu OOM, internal/validation errors and device-loss notifications are observations; device destruction is distinguished from unknown loss, and neither establishes why the process later exited.

Run cargo test -p effectcraft crash_report::tests for bounded protocol, normal shutdown, actual subprocess abort, unrelated child handles, concurrent sessions, missing log/helper, log rotation, bounded retained events, live-GUI pipe loss, and report-write failure coverage. Windows also tests a real subprocess exit with explicit 0xC0000017 bits, without exhausting memory or pretending that the exit was an actual OOM exception. The three ignored tests are subprocess entry points launched by these tests, not manual tests.

These are observations, not memory recovery or a causal diagnosis: a pipe closing does not prove an out-of-memory failure. System-wide exhaustion, killing the whole process tree, or an unavailable native dialog service can also prevent the helper from reporting. Existing auto-save recovery remains responsible for restoring previously saved work.

Native exception records, faulting-module attribution and driver/TDR confirmation require separately collected OS event evidence or a native dump. They are deliberately not gathered here: dumps can contain document contents and credentials. Any future dump workflow must be explicitly opt-in, bounded, local-first and separately reviewed before sharing. This reporter must remain useful when those sources are unavailable.