7.7 KiB
| name | description |
|---|---|
| gpui-test | 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, unlessSEEDis set. seed = N: adds a single explicit seed.seeds(...): adds multiple explicit seeds.iterations = N: runs sequential seeds starting at0by default.retries = N: retries a failing run up toNtimes before surfacing the failure.on_failure = "path::to::function": calls the function after final failure, before resuming the panic.iterationscan be combined with explicitseed/seeds; explicit seeds are appended to the0..iterationsrange.- If the
SEEDenvironment variable is set, it takes precedence over explicit seeds. - With
SEED=NandITERATIONS=Moriterations = M, the harness runs seedsN..N+M.
Environment variables
GPUI test macro / scheduler execution
SEED=<u64>— chooses the scheduler seed. Use this to reproduce a failure printed asfailing seed: N. It also seeds injectedStdRngarguments. For#[gpui::property_test], it controls the scheduler seed and GPUI applies it to the proptest config for deterministic case generation.ITERATIONS=<usize>— overrides theiterations = ...value at runtime. Use to sweep many seeds without editing the test.PENDING_TRACES=1orPENDING_TRACES=true— captures and prints pending task traces when the test scheduler panics withParking forbidden. Use this whenrun_until_parked()or teardown reports pending work.GPUI_RUN_UNTIL_PARKED_LOG=1— logs whenallow_parking()is enabled. Use to find tests that explicitly permit parking/pending work.DEBUG_SCHEDULER=1— prints scheduler clock/timer debugging fromscheduler::TestScheduler.
Lower-level scheduler tests
SCHEDULER_NONINTERACTIVE=1— suppresses interactive seed progress output inscheduler::TestScheduler::many. This does not affect the#[gpui::test]harness path.
General Rust test debugging vars often useful with GPUI tests
RUST_BACKTRACE=1orRUST_BACKTRACE=full— show panic backtraces.RUST_LOG=<filter>— 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
-
Identify the crate/package and test name.
-
Run the narrowest test filter first, skip to 3. if a failing seed is known.
cargo -q test -p <crate-name> <test_name> -- --nocapture -
If the failure mentions a seed, rerun exactly that seed.
SEED=<seed> cargo -q test -p <crate-name> <test_name> -- --nocapture -
If the failure is flaky and no seed is known, sweep seeds.
ITERATIONS=100 cargo -q test -p <crate-name> <test_name> -- --nocaptureWhen the harness prints
failing seed: <seed>, switch toSEED=<seed>for all future debugging. -
If the failure is
Parking forbidden, rerun with pending traces.PENDING_TRACES=1 cargo -q test -p <crate-name> <test_name> -- --nocaptureIf a failing seed was printed or is already known, include it too:
SEED=<seed> PENDING_TRACES=1 cargo -q test -p <crate-name> <test_name> -- --nocaptureInspect the pending traces for a task that was spawned but not awaited, detached, completed, or intentionally allowed to park.
-
If timing or timer advancement is involved, prefer GPUI scheduler timers in tests:
cx.background_executor().timer(duration).await;Avoid
smol::Timer::after(...)in GPUI tests that rely onrun_until_parked(), because GPUI's scheduler may not track it. -
Minimize the reproduction.
- Keep the failing
SEEDfixed. - Reduce
ITERATIONSto1or 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.
- Keep the failing
-
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 needTestAppContext, deterministic executors, fake time, or scheduler interleaving coverage. - Add
iterations = Nwhen the test is intentionally checking interleavings. - Use
StdRngas a test argument when randomized test data should follow the same seed as the scheduler. - Use
cx.background_executor().timer(duration).awaitfor delays/timeouts in GPUI tests. - Do not add or increase
retrieswhile 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.