Files
benilla-octo/crates/benilla-app/src/test_support.rs
T
f8e0e6c702 The trainer's ability icon shows its spell tooltip on the first hover (#158)
- Fix trainer tooltip on first hover
- The spell-tooltip feed keeps its in-game gate, and a test holds it after the trainer feed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-09-26 14:41:33 +07:00

181 lines
6.4 KiB
Rust

//! What the crate's structural tests share: source readers over its own code, and a check of
//! the order its `Update` schedule builds.
/// Every `.rs` file under `root`, recursively.
pub(crate) fn rust_files(root: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
out
}
/// This crate's `src/` directory.
pub(crate) fn src_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")
}
/// A file's path under `src/`, with forward slashes: the key every `EXEMPT` table uses.
pub(crate) fn rel_path(file: &std::path::Path) -> String {
let src = src_dir();
file.strip_prefix(&src)
.unwrap_or(file)
.to_string_lossy()
.replace('\\', "/")
}
/// One `fn` item: name, parameter list and body, each found by bracket matching.
pub(crate) struct FnItem<'a> {
pub name: &'a str,
pub params: &'a str,
pub body: &'a str,
}
/// Every `fn` in `text`; a bodiless signature comes back with an empty body.
pub(crate) fn fn_items(text: &str) -> Vec<FnItem<'_>> {
let bytes = text.as_bytes();
let mut out = Vec::new();
for (i, _) in text.match_indices("fn ") {
// `fn` must start a word: `…_fn (` and `Fn(` are not declarations.
if i > 0 && (bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_') {
continue;
}
let Some(open) = text[i..].find('(').map(|o| i + o) else {
continue;
};
let name = text[i + 3..open].trim();
let name = name.split('<').next().unwrap_or(name).trim();
let Some(close) = matching(text, open, b'(', b')') else {
continue;
};
let params = &text[open + 1..close];
// The body: the first `{` after the parameters, unless a `;` comes first.
let tail = &text[close + 1..];
let body = match (tail.find('{'), tail.find(';')) {
(Some(b), Some(s)) if s < b => "",
(Some(b), _) => {
let bopen = close + 1 + b;
match matching(text, bopen, b'{', b'}') {
Some(bclose) => &text[bopen + 1..bclose],
None => "",
}
}
(None, _) => "",
};
out.push(FnItem { name, params, body });
}
out
}
/// The parameter list of every `fn` in `text`.
pub(crate) fn fn_parameter_lists(text: &str) -> Vec<&str> {
fn_items(text).into_iter().map(|f| f.params).collect()
}
/// The index of the bracket closing the one at `open`, or `None` if it never closes.
fn matching(text: &str, open: usize, lhs: u8, rhs: u8) -> Option<usize> {
let mut depth = 0usize;
for (j, &c) in text.as_bytes()[open..].iter().enumerate() {
if c == lhs {
depth += 1;
} else if c == rhs {
depth -= 1;
if depth == 0 {
return Some(open + j);
}
}
}
None
}
/// `a` runs before `b` in `app`'s built `Update`, by system identity: a `.after()` on a system
/// missing from the schedule is silently nothing, and debug names may be off. The dependency graph
/// is walked with the set hierarchy unfolded, so a set `a` sits in precedes what that set
/// precedes, and a successor set brings its members.
pub(crate) fn runs_before<MA, MB>(
app: &mut bevy::app::App,
a: impl bevy::ecs::system::IntoSystem<(), (), MA>,
b: impl bevy::ecs::system::IntoSystem<(), (), MB>,
) -> bool {
use bevy::ecs::schedule::graph::Direction::{Incoming, Outgoing};
use bevy::ecs::schedule::NodeId;
use bevy::ecs::system::{IntoSystem, System};
use std::any::TypeId;
use std::collections::HashSet;
let a = System::type_id(&IntoSystem::into_system(a));
let b = System::type_id(&IntoSystem::into_system(b));
// `schedule_scope`, not `resource_scope::<Schedules>`: initializing the schedule inserts
// `Schedules`, which a resource scope refuses.
app.world_mut()
.schedule_scope(bevy::app::Update, |world, schedule| {
schedule
.initialize(world)
.expect("the Update schedule builds");
let key = |id: TypeId| -> NodeId {
schedule
.systems()
.expect("initialized")
.find(|&(_, s)| System::type_id(&**s) == id)
.map(|(k, _)| NodeId::System(k))
.expect("the system is in Update")
};
let (a, b) = (key(a), key(b));
let dep = schedule.graph().dependency().graph();
let hier = schedule.graph().hierarchy().graph();
let (mut after, mut containers) = (HashSet::new(), HashSet::new());
let mut work = vec![(a, false)];
while let Some((n, is_after)) = work.pop() {
let fresh = if is_after {
after.insert(n)
} else {
containers.insert(n)
};
if !fresh {
continue;
}
work.extend(dep.neighbors_directed(n, Outgoing).map(|m| (m, true)));
work.extend(hier.neighbors_directed(n, Incoming).map(|p| (p, false)));
if is_after {
work.extend(hier.neighbors_directed(n, Outgoing).map(|c| (c, true)));
}
}
after.contains(&b)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fn_comes_back_with_its_name_its_params_and_its_body() {
let text = "fn a(x: u32) -> u32 { x + 1 }\nfn b();\nfn c<T>(t: T) { if t { { } } }";
let items = fn_items(text);
assert_eq!(items.len(), 3);
assert_eq!(
(items[0].name, items[0].params, items[0].body),
("a", "x: u32", " x + 1 ")
);
assert_eq!(
(items[1].name, items[1].params, items[1].body),
("b", "", "")
);
assert_eq!(
(items[2].name, items[2].params, items[2].body),
("c", "t: T", " if t { { } } ")
);
}
}