113a933170
The repo was formatted under an older stable; rustfmt 1.9 (Rust 1.95) wraps signatures and call sites differently, so every touched file was picking up unrelated formatting hunks. One mechanical pass, and a 'cargo fmt --check' step in the test workflow (same pinned 1.95.0 toolchain) so drift can't accumulate again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
107 lines
4.5 KiB
Rust
107 lines
4.5 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).",
|
||
);
|
||
}
|
||
}
|