From 32ab74b03e9a50274396b4a32aae8baece7628a1 Mon Sep 17 00:00:00 2001 From: blasty Date: Fri, 7 Aug 2026 21:23:44 +0200 Subject: tests: gate on real signals, not sleeps (117s -> 49s) The suite spent its time in two kinds of guess. **Flat pauses.** ~140 `pause(d)` calls were 20.4s of the pilot's 62s, and `test_trace_ui` was 13.5s of `pilot.pause(1.0)` out of 19.6s. `Ctx.pause` is now `settle` (`d` is the upper bound, not the cost) and the other suites' sleeps became gates on the thing the check is about. `Ctx.sleep` stays for what a timer really drives. **Textual's keypress path.** `Pilot.press` calls `wait_for_idle` twice per key, which sleeps in 20ms granules until process time stops advancing -- 84ms per keypress here, 23s of the pilot's 43s. `_fixtures.fast_keys()` replaces it with the gate the suites already use: send the keys, then settle. Deleting the heuristic *without* that broke nine checks, so it was doing a job, badly. Four checks turned out to be riding on those sleeps: they read geometry or a repaint (`si.region`, `gv._minimap_rect()`, glyphs off `gv.render_line`, a repaint trace), and a settled app has not necessarily been laid out or painted. They now wait for the frame. The debounced function filter (`set_timer(0.08)`) likewise waits for its effect. Also fixed two waits on signals that never arrive: the comment wait in `rename` carried a `dec.loaded_ea == app._cur.ea` conjunct that cost 9s of timeout and then let the check pass vacuously, and `listing_view` -- the one entry under "Known-flaky" -- waited on `lst.total`, which is true before a single row exists. `--profile` reports, per scenario, seconds settling / waiting / pressing, and names any wait that expired with its line number. It is how the above was found and how the next 20s should be. Verified: 4 full `tests/run.py` runs, 800 passed each, 49.0-49.2s (was 117.4s); 4 consecutive pilot runs, 313 passed each, 21.2s (was 63.7s). --- tests/_fixtures.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) (limited to 'tests/_fixtures.py') diff --git a/tests/_fixtures.py b/tests/_fixtures.py index 0b72856..16192fa 100644 --- a/tests/_fixtures.py +++ b/tests/_fixtures.py @@ -22,12 +22,63 @@ doesn't silently test the previous one. `.pristine.i64` is gitignored. """ from __future__ import annotations +import asyncio import contextlib import os import shutil import tempfile +def fast_keys() -> None: + """Make a simulated keypress cost ~2ms instead of ~85ms. Call before the app. + + **The problem.** Textual sends a key and then calls ``wait_for_idle`` + *twice*, and that helper sleeps in 20ms granules until *process* time stops + advancing -- a CPU-load heuristic standing in for "the state is predictable + now", which takes even more granules on a loaded box. Measured here: 84ms + per keypress, which was 23s of the pilot suite's 43s. + + **The fix, and why it is not just deletion.** Removing the heuristic alone + broke nine checks that read state straight after a keypress -- so it *was* + doing a job, badly. This replaces it with the real gate the rest of the + suite already uses: send the keys, then ``settle`` (message pump drained, + threaded workers finished). That is strictly stronger than "the CPU looks + idle", and it is ~2ms. + + **What it still cannot see:** anything driven by a TIMER rather than a + worker -- the function-list filter's 80ms debounce, and Textual's own frame + timer (so a widget's ``region``/``size`` is not laid out just because the + app settled). Those need a wait on the effect: ``wait(lambda: rows < full)``, + ``wait(lambda: inp.region.height >= 1)``. Every such site in this repo is + commented; if a check that reads geometry or a debounced view starts + flaking, that is the reason. + + Verified equivalent, not just faster: pressing j 40 times moves 40 rows with + and without the patch, and the full suite passes with identical counts. + """ + import textual.app + from textual.pilot import Pilot + + from idatui._sync import settle + + if not hasattr(textual.app, "wait_for_idle"): # pragma: no cover + raise RuntimeError( + "textual.app.wait_for_idle is gone -- tests/_fixtures.fast_keys " + "needs updating for this Textual version") + + async def _yield_instead_of_sleeping(min_sleep: float = 0.0, + max_sleep: float = 1.0) -> None: + await asyncio.sleep(0) + + async def _press(self, *keys: str) -> None: + if keys: + await self._app._press_keys(keys) + await settle(self._app, timeout=5.0) + + textual.app.wait_for_idle = _yield_instead_of_sleeping + Pilot.press = _press + + def cache_path(binary: str) -> str: return binary + ".pristine.i64" -- cgit v1.3.1-sl0p