b402c01918
Test / test (pull_request) Failing after 17m5s
Clears the final 47 pairs and turns the ratchet into a hard gate (AMBIGUITY_BASELINE = 0, assert_eq): - HudButtons: the 14 HUD button/popover handlers run as one chain, before ui_focus::FocusKeys — Esc/keyboard consumption order is now defined (restore prompt → buttons/popovers → focus navigation → settings toggle) instead of scheduler-dependent. - HUD text updaters (update_hud, update_selection_hud, update_won_previously) chained, in UiTextFx, after the new AutoComplete set (update_hud reads AutoCompleteState). - restore_hud_on_modal → apply_hud_visibility chained before UpdateOnResize: HudVisibility writes, application, and the layout read happen in a fixed order. - New writer sets UndoRequestWriters / InfoToastWriters (same self-ambiguous pattern as NewGameRequestWriters). - Logic-before-paint: check_no_moves and the AutoComplete chain order before BoardVisuals; SettingsMutation after UpdateOnResize. - MarkerVisuals set wraps the table painter chain; chrome fx declare disjointness from it. - update_hud_typography after BoardVisuals; avatar/settings-toggle interaction handlers declared disjoint from HudButtons. 302 → 198 → 171 → 47 → 0 in four batches, one day. New systems now fail CI unless they declare their ordering or their disjointness. Closes #143 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
109 lines
4.6 KiB
Rust
109 lines
4.6 KiB
Rust
//! Schedule hygiene checks (issue #143).
|
||
//!
|
||
//! Bevy runs systems with conflicting data access in nondeterministic order
|
||
//! unless an ordering edge exists. Nothing enforced this historically, and
|
||
//! the headless gameplay cluster carries a large legacy backlog of such
|
||
//! pairs — most benign (event/resource writers that never observably race),
|
||
//! but unproven. Rather than a permanently red hard-error test or no test
|
||
//! at all, this is a RATCHET: the count may only go down. New conflicting
|
||
//! pairs fail CI immediately; the backlog can be triaged incrementally
|
||
//! (add `.before`/`.after` where order matters, `.ambiguous_with` where it
|
||
//! provably doesn't, then lower `AMBIGUITY_BASELINE`).
|
||
//!
|
||
//! To see the offending pair names, add the `bevy_debug` / debug feature to
|
||
//! the bevy dev-dependency (system names are stripped otherwise) and set
|
||
//! `ambiguity_detection: LogLevel::Error` manually.
|
||
//!
|
||
//! Scope note: `CoreGamePlugin` (the full composition) performs real
|
||
//! storage/platform I/O in `build` and cannot run in a unit test; the
|
||
//! cluster below is the same one the engine's headless behaviour tests use.
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings};
|
||
use bevy::prelude::*;
|
||
|
||
use crate::auto_complete_plugin::AutoCompletePlugin;
|
||
use crate::card_plugin::CardPlugin;
|
||
use crate::game_plugin::GamePlugin;
|
||
use crate::hud_plugin::HudPlugin;
|
||
use crate::settings_plugin::SettingsPlugin;
|
||
use crate::table_plugin::TablePlugin;
|
||
use crate::ui_focus::UiFocusPlugin;
|
||
use crate::ui_modal::UiModalPlugin;
|
||
|
||
/// The backlog (302 pairs on 2026-07-06) was burned down to ZERO the
|
||
/// same day (#143, PRs #146–#149) — this is now a hard gate. If your
|
||
/// change trips this assertion you have added a pair of systems with
|
||
/// conflicting data access and no ordering edge: add `.before`/`.after`
|
||
/// where order matters, or `.ambiguous_with` the relevant domain set
|
||
/// (BoardVisuals, MarkerVisuals, UiTextFx, HudButtons, writer sets)
|
||
/// where it provably does not. Do not raise this constant.
|
||
const AMBIGUITY_BASELINE: usize = 0;
|
||
|
||
fn cluster_app() -> App {
|
||
let mut app = App::new();
|
||
app.add_plugins(MinimalPlugins)
|
||
.add_plugins(GamePlugin)
|
||
.add_plugins(TablePlugin)
|
||
.add_plugins(CardPlugin)
|
||
.add_plugins(HudPlugin)
|
||
.add_plugins(AutoCompletePlugin)
|
||
.add_plugins(UiModalPlugin)
|
||
.add_plugins(UiFocusPlugin)
|
||
.add_plugins(SettingsPlugin::headless());
|
||
app.init_resource::<ButtonInput<KeyCode>>();
|
||
app
|
||
}
|
||
|
||
#[test]
|
||
fn update_schedule_ambiguities_do_not_grow() {
|
||
let mut app = cluster_app();
|
||
app.edit_schedule(Update, |s| {
|
||
s.set_build_settings(ScheduleBuildSettings {
|
||
ambiguity_detection: LogLevel::Error,
|
||
..Default::default()
|
||
});
|
||
});
|
||
|
||
// With LogLevel::Error the first schedule build panics when any
|
||
// ambiguity exists, and the panic message begins with the pair
|
||
// count. Catch it and ratchet on that count. (Parsing a panic
|
||
// message is brittle by design — if a Bevy upgrade rewords it,
|
||
// this test fails loudly and the parse below needs one-line
|
||
// maintenance, which is preferable to losing the ratchet.)
|
||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||
app.update();
|
||
}));
|
||
|
||
let count = match result {
|
||
Ok(()) => 0,
|
||
Err(payload) => {
|
||
let msg = payload
|
||
.downcast_ref::<String>()
|
||
.cloned()
|
||
.or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_string()))
|
||
.unwrap_or_default();
|
||
let parsed = msg
|
||
.split(" pairs of systems")
|
||
.next()
|
||
.and_then(|prefix| prefix.split_whitespace().last())
|
||
.and_then(|n| n.parse::<usize>().ok());
|
||
parsed.unwrap_or_else(|| {
|
||
panic!(
|
||
"ambiguity panic message no longer parseable (Bevy upgrade?): {msg}"
|
||
)
|
||
})
|
||
}
|
||
};
|
||
|
||
assert_eq!(
|
||
count, AMBIGUITY_BASELINE,
|
||
"system-order ambiguities changed from the enforced baseline. \
|
||
Add .before/.after or .ambiguous_with at the new registration site \
|
||
(or, if the count legitimately dropped below a nonzero baseline, \
|
||
lower AMBIGUITY_BASELINE).",
|
||
);
|
||
}
|
||
}
|