diff --git a/.agents/skills/gpui-test/SKILL.md b/.agents/skills/gpui-test/SKILL.md new file mode 100644 index 00000000000..3d92659a552 --- /dev/null +++ b/.agents/skills/gpui-test/SKILL.md @@ -0,0 +1,160 @@ +--- +name: gpui-test +description: >- + Use when writing, debugging, or reproducing GPUI tests in Zed, including + gpui::test arguments, TestAppContext parameters, scheduler seeds, + ITERATIONS/SEED reproduction, parking failures, and pending task traces. +--- + +# GPUI Test Debugging + +Use this skill when the user asks about `#[gpui::test]`, GPUI test seeds or iterations, deterministic scheduler failures, parking/pending task failures, or how to reproduce a flaky GPUI test. + +## What `#[gpui::test]` does + +`#[gpui::test]` expands to a normal Rust `#[test]`, so it runs under standard Rust test runners such as `cargo test` and `cargo nextest`. + +It wraps the body in GPUI's deterministic test dispatcher/scheduler and can run the same test multiple times with different seeds. The seed controls scheduler task interleavings and any `StdRng` argument injected into the test. + +The macro supports both synchronous and asynchronous tests. + +### Supported function arguments + +The macro recognizes arguments by type name: + +| Test kind | Supported arguments | +| --- | --- | +| Sync and async | `&TestAppContext`, `&mut TestAppContext`, `StdRng` | +| Async only | `BackgroundExecutor` | +| Sync only | `&App`, `&mut App` | + +`StdRng` is seeded from the current GPUI test seed, and `BackgroundExecutor` is backed by the same deterministic test dispatcher. + +### Attribute arguments + +Use these forms on `#[gpui::test(arguments)]`: + +- No arguments: runs once with seed `0`, unless `SEED` is set. +- `seed = N`: adds a single explicit seed. +- `seeds(...)`: adds multiple explicit seeds. +- `iterations = N`: runs sequential seeds starting at `0` by default. +- `retries = N`: retries a failing run up to `N` times before surfacing the failure. +- `on_failure = "path::to::function"`: calls the function after final failure, before resuming the panic. +- `iterations` can be combined with explicit `seed` / `seeds`; explicit seeds are appended to the `0..iterations` range. +- If the `SEED` environment variable is set, it takes precedence over explicit seeds. +- With `SEED=N` and `ITERATIONS=M` or `iterations = M`, the harness runs seeds `N..N+M`. + +## Environment variables + +### GPUI test macro / scheduler execution + +- `SEED=` — chooses the scheduler seed. Use this to reproduce a failure printed as `failing seed: N`. It also seeds injected `StdRng` arguments. For `#[gpui::property_test]`, it controls the scheduler seed and GPUI applies it to the proptest config for deterministic case generation. +- `ITERATIONS=` — overrides the `iterations = ...` value at runtime. Use to sweep many seeds without editing the test. +- `PENDING_TRACES=1` or `PENDING_TRACES=true` — captures and prints pending task traces when the test scheduler panics with `Parking forbidden`. Use this when `run_until_parked()` or teardown reports pending work. +- `GPUI_RUN_UNTIL_PARKED_LOG=1` — logs when `allow_parking()` is enabled. Use to find tests that explicitly permit parking/pending work. +- `DEBUG_SCHEDULER=1` — prints scheduler clock/timer debugging from `scheduler::TestScheduler`. + +### Lower-level scheduler tests + +- `SCHEDULER_NONINTERACTIVE=1` — suppresses interactive seed progress output in `scheduler::TestScheduler::many`. This does not affect the `#[gpui::test]` harness path. + +### General Rust test debugging vars often useful with GPUI tests + +- `RUST_BACKTRACE=1` or `RUST_BACKTRACE=full` — show panic backtraces. +- `RUST_LOG=` — enable logs when the test initializes logging. +- `ZED_HEADLESS=1` — forces GPUI platform guessing toward headless mode; useful for tests that otherwise interact with platform/window setup. + +Prefer env vars over editing the test when narrowing a reproduction. + +## Reproducing a specific GPUI test + +1. Identify the crate/package and test name. + +2. Run the narrowest test filter first, skip to 3. if a failing seed is known. + + ```sh + cargo -q test -p -- --nocapture + ``` + +3. If the failure mentions a seed, rerun exactly that seed. + + ```sh + SEED= cargo -q test -p -- --nocapture + ``` + +4. If the failure is flaky and no seed is known, sweep seeds. + + ```sh + ITERATIONS=100 cargo -q test -p -- --nocapture + ``` + + When the harness prints `failing seed: `, switch to `SEED=` for all future debugging. + +5. If the failure is `Parking forbidden`, rerun with pending traces. + + ```sh + PENDING_TRACES=1 cargo -q test -p -- --nocapture + ``` + + If a failing seed was printed or is already known, include it too: + + ```sh + SEED= PENDING_TRACES=1 cargo -q test -p -- --nocapture + ``` + + Inspect the pending traces for a task that was spawned but not awaited, detached, completed, or intentionally allowed to park. + +6. If timing or timer advancement is involved, prefer GPUI scheduler timers in tests: + + ```rust + cx.background_executor().timer(duration).await; + ``` + + Avoid `smol::Timer::after(...)` in GPUI tests that rely on `run_until_parked()`, because GPUI's scheduler may not track it. + +7. Minimize the reproduction. + - Keep the failing `SEED` fixed. + - Reduce `ITERATIONS` to `1` or remove it once a seed is known. + - Remove unrelated setup only after confirming the same seed still fails. + - Preserve scheduler-sensitive awaits/yields; removing them can mask the bug. + - If randomness is test-controlled via `StdRng`, log or assert the generated scenario after fixing the scheduler seed. + +8. Validate the fix. + - Run the fixed seed. + - Run a modest seed sweep, e.g. `ITERATIONS=20`, if the failure was scheduler-sensitive. + - Run the relevant crate's test filter or broader suite if the touched code has shared behavior. + +## Common diagnosis patterns + +### Seed-dependent assertion failure + +Likely caused by a scheduler interleaving or by `StdRng`-driven test data. Fix `SEED`, reproduce, and inspect which task or generated scenario differs. + +### `Parking forbidden` + +Usually means a foreground/background task is still pending when the scheduler expected the test to make progress or finish. Look for: + +- A task that should be awaited but was dropped. +- A task that should be detached with error logging. +- A timer or receiver that is waiting forever. +- A missing `cx.run_until_parked()` after triggering async work in a test. +- A missing `cx.advance_clock(...)` to wait for debounced work in a test. +- Use of non-GPUI timers or executors that the test scheduler cannot drive. + +Rerun with `PENDING_TRACES=1` before changing code. + +### Non-determinism / wrong thread + +The scheduler can report activity from an unexpected thread. Look for work escaping GPUI's foreground/background executors, direct thread spawns, or external async runtimes not controlled by the test dispatcher. + +### Tests pass alone but fail in sweeps + +Use the failing seed from sweep output. Avoid assuming test order unless the runner is explicitly serial. Check globals, leaked entities/tasks, and state not reset by test initialization. + +## Writing GPUI tests + +- Prefer `#[gpui::test]` for tests that need `TestAppContext`, deterministic executors, fake time, or scheduler interleaving coverage. +- Add `iterations = N` when the test is intentionally checking interleavings. +- Use `StdRng` as a test argument when randomized test data should follow the same seed as the scheduler. +- Use `cx.background_executor().timer(duration).await` for delays/timeouts in GPUI tests. +- Do not add or increase `retries` while fixing a test unless the user explicitly asks or the test already documents why probabilistic tolerance is intentional. Retries can mask the failure instead of fixing it.