fix(gpu): 1-pre round 2 --- a read ceiling, and the A4 consequence was wrong
Two review findings, one of them a real defect. **THE SENTINEL READ COULD HANG FOREVER.** `read_until_sentinel` blocked with no bound, so a writer or encoder that regressed after `enqueue` would WEDGE THE GATE rather than redden it --- and a hang is the worst failure shape there is, because it looks like slowness until the job is killed. A 30 s `READ_CEILING` is armed on the daemon socket. The distinction is kept explicit in the code, because collapsing it is how this fix would undo the design it protects: **the sentinel remains the success condition and the ceiling is only an error ceiling.** Arrival is still decided by the sentinel, so the harness never infers "nothing was sent" from a duration --- the core-count assumption behind PR #235's CI red is not reintroduced. The ceiling sits far above any plausible drain, so reaching it means broken, never busy. M24 proves it fires rather than trusting it: drop the sentinel enqueue entirely and the row fails in under a second with a diagnostic naming both candidate causes and the partial transcript, instead of hanging. **THE STAGE 1a CONSEQUENCE WAS WRONG IN FOUR PLACES.** Every record claimed A4 would leave `EventOutcome` with one variant, so the type should go with the Escape branch. It will not, and it should not. `LifecycleRoute::Exit` --- a native window close --- returns `EventOutcome::Exit` too. A4 removes the KEYBOARD producer only, leaving one `Exit` producer. And **one producer is not one variant**: the type survives because `dispatch_window_event` must still distinguish `Continue` from `Exit` on every event it handles --- nearly all must not exit, and the close must. What A4 actually changes is `apply_keyboard`'s signature. Corrected in the `EventOutcome` doc, the Escape-branch comment, the framing and the ledger; the framing's superseded paragraph is deleted rather than patched, since it also carried the stale "two `event_loop.exit()` call sites" count. **There is exactly one executable `event_loop.exit()`**, in `window_event`. Also: the sentinel-tag comment claimed four modifier bits and used three. It now says three, wrapping every eight steps, and why that suffices --- each sentinel is read before the next is issued, so a tag only has to differ from its immediate predecessor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
68c99fb2e2
commit
9f08f5278c
|
|
@ -268,10 +268,11 @@ waits for a signal that is not coming.
|
||||||
Stage 1's framing for **all** slices and governs the later branches;
|
Stage 1's framing for **all** slices and governs the later branches;
|
||||||
only Stage 0 was framed by the arc document itself.
|
only Stage 0 was framed by the arc document itself.
|
||||||
- **Scope of THIS branch: 1-pre only — the input seam. No behaviour
|
- **Scope of THIS branch: 1-pre only — the input seam. No behaviour
|
||||||
change.** `App::window_event` was **655 lines** and is now **33**: one
|
change.** `App::window_event` was **655 lines** and is now **four** —
|
||||||
`route_event` call and one arm per route. Nothing below it could be
|
call `dispatch_window_event`, exit if it asks. The dispatch it calls
|
||||||
witnessed without a display, which is why the seam precedes every
|
is one `route_event` call and one arm per route. Nothing below it
|
||||||
other slice.
|
could be witnessed without a display, which is why the seam precedes
|
||||||
|
every other slice.
|
||||||
- **IMPLEMENTED in four commits, one per event family**, so each lands
|
- **IMPLEMENTED in four commits, one per event family**, so each lands
|
||||||
with its own witnesses and mutations rather than as one 600-line
|
with its own witnesses and mutations rather than as one 600-line
|
||||||
diff: `014110f` lifecycle (close / modifiers / resize), `7f0f9db`
|
diff: `014110f` lifecycle (close / modifiers / resize), `7f0f9db`
|
||||||
|
|
@ -321,22 +322,26 @@ waits for a signal that is not coming.
|
||||||
free function composing one decision function per family
|
free function composing one decision function per family
|
||||||
(`route_lifecycle`, `route_keyboard` + `route_key_action`,
|
(`route_lifecycle`, `route_keyboard` + `route_key_action`,
|
||||||
`route_pointer`). Performing stays on `App` in seven `apply_*`
|
`route_pointer`). Performing stays on `App` in seven `apply_*`
|
||||||
methods. **A route names its LOCAL EFFECT**, not merely the family
|
methods. A route carries the **decision**, not the effect: a wheel
|
||||||
that claims it, and the harness records routes — because
|
route holds a delta, and whether that becomes a viewport update, a
|
||||||
`CloseRequested` and `RedrawRequested` send the daemon nothing, so a
|
panel event, a terminal event or nothing depends on `State`. Effects
|
||||||
transcript of protocol traffic could not tell a handled arm from a
|
are witnessed separately — see the P2 bullet below.
|
||||||
dropped one.
|
|
||||||
- **Every moved body verified as the original, mechanically.** The
|
- **Every moved body verified as the original, mechanically.** The
|
||||||
keyboard body is byte-identical modulo two named conversions; the four
|
keyboard body is byte-identical modulo two named conversions; the four
|
||||||
pointer bodies were checked by re-running rustfmt on the pre-move text
|
pointer bodies were checked by re-running rustfmt on the pre-move text
|
||||||
at the new indent level and diffing, since de-indenting by 8 columns
|
at the new indent level and diffing, since de-indenting by 8 columns
|
||||||
lets rustfmt rejoin lines. **13 witnesses, 17 mutations.**
|
lets rustfmt rejoin lines. **22 witnesses — 13 routing, 9 effect — and
|
||||||
- **P3 IS NOW MEASURED, NOT ASSUMED.** Replacing `window_event`'s whole
|
23 mutations**, each failing its own rows, plus the P3 exception check
|
||||||
body with `let _ = (event_loop, event);` — a GUI that responds to no
|
which must stay green.
|
||||||
input at all — leaves **all 256 `pmacs-gpu` tests green**. That is the
|
- **P3 IS NOW MEASURED, NOT ASSUMED — and re-measured after the effect
|
||||||
exception's true extent: `ActiveEventLoop` cannot exist outside a live
|
harness landed.** Replacing `window_event`'s whole body with `let _ =
|
||||||
event loop, so **no** headless test in the crate observes the
|
(event_loop, event);` — a GUI that responds to no input at all —
|
||||||
delegation, not merely none of the new ones.
|
leaves **all 265 `pmacs-gpu` tests green** under `PMACS_REQUIRE_GPU=1`
|
||||||
|
at the current shape (it was 256 before the effect rows; the number is
|
||||||
|
re-run, not carried forward). That is the exception's true extent:
|
||||||
|
`ActiveEventLoop` cannot exist outside a live event loop, so **no**
|
||||||
|
headless test in the crate observes the delegation. What it covers is
|
||||||
|
now **one `if`**, not a 33-line match.
|
||||||
- **A SECOND ACCEPTED STRUCTURAL EXCEPTION, found here and winit's
|
- **A SECOND ACCEPTED STRUCTURAL EXCEPTION, found here and winit's
|
||||||
rather than ours.** `KeyEvent` carries a `pub(crate)
|
rather than ours.** `KeyEvent` carries a `pub(crate)
|
||||||
platform_specific` field, so **no `WindowEvent::KeyboardInput` can be
|
platform_specific` field, so **no `WindowEvent::KeyboardInput` can be
|
||||||
|
|
@ -347,12 +352,19 @@ waits for a signal that is not coming.
|
||||||
the exception down); the family's only real decision is factored into
|
the exception down); the family's only real decision is factored into
|
||||||
`route_key_action(ElementState)` and witnessed directly; and what
|
`route_key_action(ElementState)` and witnessed directly; and what
|
||||||
stays unwitnessed is one pattern arm containing a match and a call.
|
stays unwitnessed is one pattern arm containing a match and a call.
|
||||||
- **`event_loop.exit()` now appears in exactly two places, both inside
|
- **The crate now has exactly ONE executable `event_loop.exit()`**, in
|
||||||
`window_event`.** The keyboard body was its second caller (the idle
|
`window_event`. Bodies return an `EventOutcome` rather than taking an
|
||||||
Escape), so `apply_keyboard` returns an `EventOutcome` rather than
|
`&ActiveEventLoop`, which is what keeps them reachable in principle.
|
||||||
taking an `&ActiveEventLoop` — which is what keeps the bodies
|
**`EventOutcome` has TWO producers and they differ in kind**: a native
|
||||||
reachable in principle. **Stage 1a's A4 deletes that branch**, at
|
close (`LifecycleRoute::Exit`), which must always exit, and
|
||||||
which point `EventOutcome` has one variant and should go.
|
`apply_keyboard`'s idle Escape, which is a local quit. **Stage 1a's A4
|
||||||
|
removes the keyboard producer only**, leaving **one** `Exit`
|
||||||
|
producer — the native close. **One producer is not one variant**:
|
||||||
|
`EventOutcome` survives because `dispatch_window_event` must still
|
||||||
|
distinguish `Continue` from `Exit` on every event, and only the close
|
||||||
|
exits. A4 changes `apply_keyboard`'s signature, not the type. An
|
||||||
|
earlier revision of this bullet claimed the type would collapse and
|
||||||
|
should go with the Escape branch; that was wrong.
|
||||||
- **Slice order (each its own branch and PR):** `1-pre` → `1a`\* → `1b`
|
- **Slice order (each its own branch and PR):** `1-pre` → `1a`\* → `1b`
|
||||||
→ `1c` → `1d` → `1e`\*. **`1a` and `1e` are protocol-bearing (v24
|
→ `1c` → `1d` → `1e`\*. **`1a` and `1e` are protocol-bearing (v24
|
||||||
`TextInput`, v25 `OpenTarget`/`OpenTargetResult`) and are
|
`TextInput`, v25 `OpenTarget`/`OpenTargetResult`) and are
|
||||||
|
|
@ -374,10 +386,11 @@ waits for a signal that is not coming.
|
||||||
file-watcher tests — with every other target green. Established as
|
file-watcher tests — with every other target green. Established as
|
||||||
environmental three ways, in increasing strength:
|
environmental three ways, in increasing strength:
|
||||||
- **Structurally impossible for this branch to cause.** The branch
|
- **Structurally impossible for this branch to cause.** The branch
|
||||||
changes six files, five of them under `docs/`; **the whole
|
changes seven files, five of them under `docs/`; **the whole
|
||||||
executable diff is `pmacs-gpu/src/main.rs`**, and `pmacs-gpu` is a
|
executable diff is inside the `pmacs-gpu` crate**
|
||||||
workspace **member but not a dependency** of the root package, so
|
(`src/main.rs` and `src/attach.rs`), and `pmacs-gpu` is a workspace
|
||||||
the `m4_acceptance` binary never links it.
|
**member but not a dependency** of the root package, so the
|
||||||
|
`m4_acceptance` binary never links it.
|
||||||
- **The marker is older than the session.** `/tmp/.git` is empty and
|
- **The marker is older than the session.** `/tmp/.git` is empty and
|
||||||
was created at 20:14 CEST, **3.5 h before** the gate run at 23:45;
|
was created at 20:14 CEST, **3.5 h before** the gate run at 23:45;
|
||||||
`/tmp` held 8,920 entries. That is handoff §1's recorded hazard
|
`/tmp` held 8,920 entries. That is handoff §1's recorded hazard
|
||||||
|
|
@ -400,12 +413,21 @@ waits for a signal that is not coming.
|
||||||
unnecessary, and the isolated `TMPDIR` is the correct remedy. It must
|
unnecessary, and the isolated `TMPDIR` is the correct remedy. It must
|
||||||
be **outside `/tmp` and outside every git worktree** — a child of
|
be **outside `/tmp` and outside every git worktree** — a child of
|
||||||
`/tmp` is not isolated, because `/tmp/.git` remains its ancestor.
|
`/tmp` is not isolated, because `/tmp/.git` remains its ancestor.
|
||||||
- **GREEN under an isolated `TMPDIR`: all nine gates pass** (log
|
- **GREEN under an isolated `TMPDIR`: all nine gates pass on the final
|
||||||
`20260811T215605Z-2664352`). fmt, clippy, lib, lib-crdt,
|
tree** (log `20260812T090034Z-2989598`, review round 2). fmt, clippy,
|
||||||
`gpu_invocation_acceptance`, **m4 168/0/3**, `PMACS_REQUIRE_GPU=1 -p
|
lib, lib-crdt, `gpu_invocation_acceptance`, **m4 168/0/3**,
|
||||||
pmacs-gpu`, the **117-target `--workspace --no-fail-fast` sweep with
|
`PMACS_REQUIRE_GPU=1 -p pmacs-gpu` (**265 tests, none filtered and
|
||||||
zero failures anywhere** (`m4_acceptance` running all 171 in it), and
|
none skipped**, all nine effect rows included), the **117-target
|
||||||
`diff-check`.
|
`--workspace --no-fail-fast` sweep with zero failures anywhere**
|
||||||
|
(`m4_acceptance` running all 171 in it), and `diff-check`. Earlier
|
||||||
|
full-green runs at `20260811T215605Z-2664352` (round 1) and
|
||||||
|
`20260812T083735Z-2869707` (the P2 harness) are superseded by this
|
||||||
|
one. **What changed after it is doc comments and prose only** — the
|
||||||
|
`EventOutcome` correction below and this paragraph; `cargo fmt
|
||||||
|
--check`, `clippy -D warnings` and the 22 routing/effect rows were
|
||||||
|
re-run on the result. Stated rather than glossed, because "the gate
|
||||||
|
was green" and "the gate was green on exactly this tree" are
|
||||||
|
different claims.
|
||||||
|
|
||||||
## The GUI arc — Stage 0 MERGED as #236 (`f8ad3e7`)
|
## The GUI arc — Stage 0 MERGED as #236 (`f8ad3e7`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
roots but **not `TMPDIR`**, so `tempfile::tempdir()` still lands
|
roots but **not `TMPDIR`**, so `tempfile::tempdir()` still lands
|
||||||
under a `/tmp` that may carry a marker. It surfaced inside a gate
|
under a `/tmp` that may carry a marker. It surfaced inside a gate
|
||||||
run on an unrelated lane (GUI 1-pre, whose whole **executable** diff
|
run on an unrelated lane (GUI 1-pre, whose whole **executable** diff
|
||||||
is `pmacs-gpu/src/main.rs`) as **`m4_24_bare_string_glob_stays_relative`
|
is inside the `pmacs-gpu` crate) as **`m4_24_bare_string_glob_stays_relative`
|
||||||
and `m4_24_d3_fallback_base_is_the_smallest_attachment_dir`**, in
|
and `m4_24_d3_fallback_base_is_the_smallest_attachment_dir`**, in
|
||||||
both the `m4` step and the `--workspace` sweep, with every other
|
both the `m4` step and the `--workspace` sweep, with every other
|
||||||
target in the corpus green.
|
target in the corpus green.
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,14 @@ blanket one:
|
||||||
directly, with both misroute mutations failing that row alone.
|
directly, with both misroute mutations failing that row alone.
|
||||||
- **P3 is now measured, not assumed.** Replacing `window_event`'s entire
|
- **P3 is now measured, not assumed.** Replacing `window_event`'s entire
|
||||||
body with `let _ = (event_loop, event);` — a GUI that responds to no
|
body with `let _ = (event_loop, event);` — a GUI that responds to no
|
||||||
input at all — leaves **all 256 `pmacs-gpu` tests green**, not merely
|
input at all — leaves **every `pmacs-gpu` test green**. That is the
|
||||||
the 13 routing rows. That is the exception's true extent: no headless
|
exception's true extent: no headless test anywhere in the crate
|
||||||
test anywhere in the crate observes the delegation. *(Revision 11
|
observes the delegation. **Re-measured at the current shape after
|
||||||
shrinks what the exception covers: `window_event` is now four lines,
|
revision 11: 265/265 under `PMACS_REQUIRE_GPU=1`** (it was 256 before
|
||||||
so the unwitnessed residue is one `if` rather than a 33-line match.)*
|
the effect rows existed, and the number is re-run rather than carried
|
||||||
|
forward). Revision 11 also shrinks what the exception *covers*:
|
||||||
|
`window_event` is four lines, so the unwitnessed residue is one `if`
|
||||||
|
rather than a 33-line match.
|
||||||
|
|
||||||
**Revision 11 — P2 IS IMPLEMENTED AS WRITTEN. Revision 10's argument
|
**Revision 11 — P2 IS IMPLEMENTED AS WRITTEN. Revision 10's argument
|
||||||
here was wrong and is retracted.**
|
here was wrong and is retracted.**
|
||||||
|
|
@ -194,17 +197,30 @@ directions, which is P2's contract executable rather than asserted.
|
||||||
|
|
||||||
The routing rows stay, and the division of labour is deliberate: the
|
The routing rows stay, and the division of labour is deliberate: the
|
||||||
routing harness answers *where did this event go*, the effect harness
|
routing harness answers *where did this event go*, the effect harness
|
||||||
answers *what did it do*. The transcript row remains the routing
|
answers *what did it do*. **P2 is owned by the effect rows.** The
|
||||||
harness's sole P2-recording owner, which keeps its own mutation
|
routing transcript row is only the sole owner of the *routing*
|
||||||
surgical.
|
harness's own recording, which is what keeps that one mutation
|
||||||
|
surgical — revision 10 claimed it owned P2, and it never did.
|
||||||
|
|
||||||
**One design consequence worth carrying into 1a.** The keyboard arm was
|
**One further correction revision 11 carries, and it is the design
|
||||||
the second caller of `event_loop.exit()` — the idle-Escape local quit —
|
consequence 1a inherits.** Revision 10 stated that Stage 1a's A4 would
|
||||||
so its body returns an `EventOutcome` rather than taking an
|
leave `EventOutcome` with a single variant and that the type should go
|
||||||
`&ActiveEventLoop`. `event_loop.exit()` now appears in exactly two
|
with the Escape branch. **Both are wrong.**
|
||||||
places, both inside `window_event`, and nowhere else in the crate.
|
|
||||||
**A4 deletes the Escape branch, at which point `EventOutcome` has one
|
`EventOutcome` has **two producers today**: `LifecycleRoute::Exit`, a
|
||||||
variant and should go with it.**
|
native window close that must always exit, and `apply_keyboard`'s idle
|
||||||
|
Escape, a local quit. **A4 removes the keyboard one**, leaving **exactly
|
||||||
|
one `Exit` producer** — the native close.
|
||||||
|
|
||||||
|
**One producer is not one variant.** The type survives because
|
||||||
|
`dispatch_window_event` still has to distinguish `Continue` from
|
||||||
|
`Exit` on every event it handles: the overwhelming majority of
|
||||||
|
dispatches must *not* exit, and the native close must. What A4 actually
|
||||||
|
removes is `apply_keyboard`'s need to return an outcome at all, which is
|
||||||
|
a change to that one signature rather than to this type.
|
||||||
|
|
||||||
|
The crate has **exactly one** executable `event_loop.exit()`, in
|
||||||
|
`window_event`.
|
||||||
|
|
||||||
### 1a — `TextInput` (v24)
|
### 1a — `TextInput` (v24)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3226,8 +3226,9 @@ impl App {
|
||||||
} else {
|
} else {
|
||||||
// Q#S1-1 / A4 — the local quit, unchanged here and
|
// Q#S1-1 / A4 — the local quit, unchanged here and
|
||||||
// deleted by Stage 1a: an idle Escape must reach the
|
// deleted by Stage 1a: an idle Escape must reach the
|
||||||
// daemon. `window_event` performs the exit; this is
|
// daemon. `window_event` performs the exit. This is the
|
||||||
// the only reason a body needs an outcome at all.
|
// only reason a BODY needs an outcome; `EventOutcome`
|
||||||
|
// itself outlives A4, since a native close still exits.
|
||||||
return EventOutcome::Exit;
|
return EventOutcome::Exit;
|
||||||
}
|
}
|
||||||
return EventOutcome::Continue;
|
return EventOutcome::Continue;
|
||||||
|
|
@ -3395,13 +3396,11 @@ impl App {
|
||||||
/// exception and how far it reaches. **Performing** stays on `App`, in
|
/// exception and how far it reaches. **Performing** stays on `App`, in
|
||||||
/// the `apply_*` methods the router's variants name.
|
/// the `apply_*` methods the router's variants name.
|
||||||
///
|
///
|
||||||
/// The decision is what the route *is*, not merely which family claims
|
/// A route carries the decision, not merely the family: `Resize` holds
|
||||||
/// it: `Exit` is the local exit effect, `Resize` carries the clamped
|
/// the clamped extent, `Modifiers` the new state. **What a route does
|
||||||
/// surface extent, `Modifiers` carries the state mutation. **Two arms —
|
/// NOT carry is the effect** — a `Wheel` may become a viewport update, a
|
||||||
/// `CloseRequested` and `RedrawRequested` — send nothing outbound at
|
/// panel event, a terminal event or nothing at all, depending on
|
||||||
/// all**, so a harness recording only protocol traffic would leave them
|
/// `State`. Effects are witnessed separately, by `EffectHarness`.
|
||||||
/// invisible; that is why a route names its local effect and the harness
|
|
||||||
/// records routes.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum Route<'a> {
|
enum Route<'a> {
|
||||||
/// The lifecycle family — see [`route_lifecycle`].
|
/// The lifecycle family — see [`route_lifecycle`].
|
||||||
|
|
@ -3417,16 +3416,25 @@ enum Route<'a> {
|
||||||
Unrouted,
|
Unrouted,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What the event loop must do once a family's body has run. Only the
|
/// What the event loop must do once a family's body has run.
|
||||||
/// keyboard family produces anything but `Continue` today: an idle
|
|
||||||
/// Escape is a local quit. Returning the decision rather than taking an
|
|
||||||
/// `&ActiveEventLoop` is what keeps every body reachable from a test —
|
|
||||||
/// the crate's two `event_loop.exit()` call sites, this one and
|
|
||||||
/// `LifecycleRoute::Exit`, both sit in `window_event` and nowhere else.
|
|
||||||
///
|
///
|
||||||
/// Stage 1a's A4 deletes that branch (an idle Escape must reach the
|
/// **Two producers, and they are not the same kind of thing.**
|
||||||
/// daemon and never exit), at which point this type has one variant and
|
/// `LifecycleRoute::Exit` is a native window close, which must always
|
||||||
/// should go.
|
/// exit; `apply_keyboard` returns `Exit` for an idle Escape, which is a
|
||||||
|
/// local quit. Returning the decision rather than taking an
|
||||||
|
/// `&ActiveEventLoop` is what keeps every body reachable from a test:
|
||||||
|
/// the crate has **exactly one** executable `event_loop.exit()`, in
|
||||||
|
/// `window_event`.
|
||||||
|
///
|
||||||
|
/// **Stage 1a's A4 removes the KEYBOARD producer only** — an idle
|
||||||
|
/// Escape must reach the daemon and never exit — leaving **one** `Exit`
|
||||||
|
/// producer, the native close.
|
||||||
|
///
|
||||||
|
/// **One producer is not one variant.** This type survives A4 because
|
||||||
|
/// `dispatch_window_event` must still distinguish `Continue` from
|
||||||
|
/// `Exit` on every event it handles: nearly all of them must not exit,
|
||||||
|
/// and the close must. What A4 changes is `apply_keyboard`'s signature,
|
||||||
|
/// not this type.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum EventOutcome {
|
enum EventOutcome {
|
||||||
Continue,
|
Continue,
|
||||||
|
|
@ -3585,7 +3593,7 @@ fn route_pointer(event: &WindowEvent) -> Option<PointerRoute> {
|
||||||
/// deliberately, not of outbound protocol traffic: `CloseRequested`
|
/// deliberately, not of outbound protocol traffic: `CloseRequested`
|
||||||
/// exits and `RedrawRequested` repaints, and neither sends the daemon
|
/// exits and `RedrawRequested` repaints, and neither sends the daemon
|
||||||
/// anything, so a transcript of daemon traffic alone cannot tell a
|
/// anything, so a transcript of daemon traffic alone cannot tell a
|
||||||
/// handled arm from a dropped one. A route names its local effect —
|
/// handled arm from a dropped one. A route records the decision —
|
||||||
/// exit, resize extent, modifier mutation — which is what makes those
|
/// exit, resize extent, modifier mutation — which is what makes those
|
||||||
/// arms observable here at all.
|
/// arms observable here at all.
|
||||||
///
|
///
|
||||||
|
|
@ -3691,6 +3699,13 @@ impl EffectHarness {
|
||||||
/// anything a body would legitimately send.
|
/// anything a body would legitimately send.
|
||||||
const SENTINEL: char = '\u{e000}';
|
const SENTINEL: char = '\u{e000}';
|
||||||
|
|
||||||
|
/// Error ceiling on one outbound read. **Not a pacing device** — the
|
||||||
|
/// sentinel decides arrival — so this is set far above any plausible
|
||||||
|
/// drain: reaching it means the writer or encoder is broken, never
|
||||||
|
/// that the machine is busy. Without it a regression downstream of
|
||||||
|
/// `enqueue` wedges the gate instead of reddening it.
|
||||||
|
const READ_CEILING: std::time::Duration = std::time::Duration::from_secs(30);
|
||||||
|
|
||||||
/// Build the harness, or fail loudly.
|
/// Build the harness, or fail loudly.
|
||||||
///
|
///
|
||||||
/// **Never skips.** `State::new_headless` returns `None` when no
|
/// **Never skips.** `State::new_headless` returns `None` when no
|
||||||
|
|
@ -3717,6 +3732,9 @@ impl EffectHarness {
|
||||||
let client = crate::attach::connect_stream_for_test(client_stream, |_| true)
|
let client = crate::attach::connect_stream_for_test(client_stream, |_| true)
|
||||||
.expect("attach over socketpair");
|
.expect("attach over socketpair");
|
||||||
let daemon = handshake.join().expect("handshake thread");
|
let daemon = handshake.join().expect("handshake thread");
|
||||||
|
daemon
|
||||||
|
.set_read_timeout(Some(Self::READ_CEILING))
|
||||||
|
.expect("arm the outbound read ceiling");
|
||||||
|
|
||||||
// A document tall enough to scroll. A two-line fixture made the
|
// A document tall enough to scroll. A two-line fixture made the
|
||||||
// wheel row pass vacuously: `scroll_by_lines` returns `None`
|
// wheel row pass vacuously: `scroll_by_lines` returns `None`
|
||||||
|
|
@ -3822,10 +3840,17 @@ impl EffectHarness {
|
||||||
/// Push a sentinel through the same outbox and read until it comes
|
/// Push a sentinel through the same outbox and read until it comes
|
||||||
/// back. Everything ahead of it belongs to the step just dispatched.
|
/// back. Everything ahead of it belongs to the step just dispatched.
|
||||||
///
|
///
|
||||||
/// This is a **condition**, not a sleep: it blocks on the socket
|
/// **The sentinel is the success condition, and the timeout is only
|
||||||
/// until the writer thread has drained past the sentinel, so it is
|
/// an error ceiling** — the two are not the same thing and the
|
||||||
/// insensitive to how many cores are free — the mistake PR #235's CI
|
/// distinction is the whole design. Arrival is decided by the
|
||||||
/// red was made of.
|
/// sentinel, so the harness never infers "nothing was sent" from a
|
||||||
|
/// duration and is insensitive to how many cores are free; that is
|
||||||
|
/// the mistake PR #235's CI red was made of. But a blocking read
|
||||||
|
/// with no bound turns a regressed writer or encoder into a **wedged
|
||||||
|
/// gate** rather than a red one, and a hang is the worst failure
|
||||||
|
/// shape there is: it looks like slowness until the job is killed.
|
||||||
|
/// [`Self::READ_CEILING`] is therefore set far above any plausible
|
||||||
|
/// drain, so reaching it means broken, never busy.
|
||||||
fn read_until_sentinel(&mut self) -> Vec<pmacs_protocol::FrontendEvent> {
|
fn read_until_sentinel(&mut self) -> Vec<pmacs_protocol::FrontendEvent> {
|
||||||
self.sentinel_seq += 1;
|
self.sentinel_seq += 1;
|
||||||
let tag = self.sentinel_seq;
|
let tag = self.sentinel_seq;
|
||||||
|
|
@ -3837,7 +3862,16 @@ impl EffectHarness {
|
||||||
let mut seen = Vec::new();
|
let mut seen = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
let event: pmacs_protocol::FrontendEvent =
|
let event: pmacs_protocol::FrontendEvent =
|
||||||
pmacs_protocol::read_message(&mut self.daemon).expect("read outbound");
|
match pmacs_protocol::read_message(&mut self.daemon) {
|
||||||
|
Ok(event) => event,
|
||||||
|
Err(e) => panic!(
|
||||||
|
"outbound read failed before the sentinel arrived \
|
||||||
|
after {:?}: {e}. Either the writer or the encoder \
|
||||||
|
regressed, or the step produced no sentinel at all. \
|
||||||
|
Recorded so far: {seen:?}",
|
||||||
|
Self::READ_CEILING
|
||||||
|
),
|
||||||
|
};
|
||||||
if is_sentinel(&event, tag) {
|
if is_sentinel(&event, tag) {
|
||||||
return seen;
|
return seen;
|
||||||
}
|
}
|
||||||
|
|
@ -3856,9 +3890,11 @@ struct EffectSnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modifier bits carrying the sentinel's sequence number, so a stale
|
/// Modifier bits carrying the sentinel's sequence number, so a stale
|
||||||
/// sentinel cannot end the wrong step. Four bits is plenty: the harness
|
/// sentinel cannot end the wrong step. **Three bits — Ctrl, Alt, Shift —
|
||||||
/// reads every sentinel it writes, so the counter only has to
|
/// so the tag wraps every eight steps.** That is sufficient rather than
|
||||||
/// distinguish neighbours.
|
/// sloppy: the harness reads every sentinel it writes before issuing the
|
||||||
|
/// next, so a tag only ever has to distinguish itself from the one
|
||||||
|
/// immediately before it.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn sentinel_mods(tag: u32) -> Modifiers {
|
fn sentinel_mods(tag: u32) -> Modifiers {
|
||||||
let mut mods = Modifiers::NONE;
|
let mut mods = Modifiers::NONE;
|
||||||
|
|
@ -3915,10 +3951,10 @@ mod input_routing_tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The per-variant rows below drive [`route_event`] directly and the
|
/// The per-variant rows below drive [`route_event`] directly and the
|
||||||
/// transcript row drives the harness. That split is deliberate: it
|
/// transcript row drives [`RoutingHarness`]. That split keeps the
|
||||||
/// leaves the transcript row as P2's sole owner, so a harness that
|
/// transcript row the only one that fails when the ROUTING harness
|
||||||
/// stopped recording an effect fails exactly one row instead of
|
/// stops recording, so that mutation stays surgical. **P2 itself is
|
||||||
/// every row.
|
/// owned by the effect rows** — see [`EffectHarness`].
|
||||||
///
|
///
|
||||||
/// Events are bound to locals rather than passed as temporaries
|
/// Events are bound to locals rather than passed as temporaries
|
||||||
/// because a `Route` borrows the event it came from — the keyboard
|
/// because a `Route` borrows the event it came from — the keyboard
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue