egui037: reconcile current main and finalize published dependency pins

This commit is contained in:
skylartaylor
2026-10-24 23:21:27 +02:00
79 changed files with 4961 additions and 368 deletions
+30 -9
View File
@@ -49,25 +49,35 @@ struct Entry {
kind: String,
target: String,
pending: bool,
/// Fixed params as a compact JSON object (`params` in keytips.json), empty for none.
params: String,
excel: String,
note: String,
}
impl Entry {
/// The probe key: a command and the params its keytip passes.
fn probe_key(&self) -> String {
if self.params.is_empty() || self.params == "{}" { self.target.clone() } else { format!("{} {}", self.target, self.params) }
}
}
/// 0 = ran and journaled, 1 = ran without a journal step, 2 = errored (params
/// missing, disabled, or environment-dependent).
/// missing, disabled, or environment-dependent). Keyed by [`Entry::probe_key`].
type Probe = std::collections::HashMap<String, u8>;
#[cfg(feature = "engine")]
fn probe_live_commands(ids: &[String]) -> Probe {
fn probe_live_commands(calls: &[(String, String)]) -> Probe {
use gridcraft_engine::Session;
let mut map = Probe::new();
for id in ids {
for (id, params) in calls {
if ENV_SENSITIVE.contains(&id.as_str()) || UI_DIALOG_COMMANDS.iter().any(|(c, _)| c == id) {
continue;
}
let params = if params.is_empty() { serde_json::json!({}) } else { serde_json::from_str(params).unwrap_or_default() };
let mut s = Session::new();
s.new_workbook();
let status = match s.execute(id, serde_json::json!({})) {
let status = match s.execute(id, params.clone()) {
Ok(_) => {
if s.journal.iter().any(|(jid, _)| jid == id) {
0
@@ -77,7 +87,8 @@ fn probe_live_commands(ids: &[String]) -> Probe {
}
Err(_) => 2,
};
map.insert(id.clone(), status);
let key = if params.as_object().is_some_and(|o| !o.is_empty()) { format!("{id} {params}") } else { id.clone() };
map.insert(key, status);
}
map
}
@@ -137,6 +148,13 @@ fn parse_and_probe(source: &str) -> Result<(Vec<Entry>, Probe), String> {
if pending && kind != "command" {
return Err(format!("{at}: `pending` is only valid with kind `command`"));
}
let params = match e.get("params") {
None => String::new(),
Some(v) if v.is_object() && matches!(kind.as_str(), "command" | "dialog") && !pending => {
serde_json::to_string(v).map_err(|e| format!("{at}: params: {e}"))?
}
Some(_) => return Err(format!("{at}: `params` must be a JSON object, on a non-pending command or dialog")),
};
match kind.as_str() {
"tab" => {
if target.is_empty() || !TABS.contains(&target.as_str()) {
@@ -168,6 +186,7 @@ fn parse_and_probe(source: &str) -> Result<(Vec<Entry>, Probe), String> {
kind,
target,
pending,
params,
excel: e.get("excel").and_then(|v| v.as_str()).unwrap_or("").to_string(),
note: e.get("note").and_then(|v| v.as_str()).unwrap_or("").to_string(),
});
@@ -175,10 +194,10 @@ fn parse_and_probe(source: &str) -> Result<(Vec<Entry>, Probe), String> {
// JSON order is authoritative and curated: the first entry for a tab is its canonical
// badge (legacy aliases come later), and groups stay together. Do not sort.
let live_ids: Vec<String> = entries
let live_calls: Vec<(String, String)> = entries
.iter()
.filter(|e| e.kind == "command" && !e.pending)
.map(|e| e.target.clone())
.map(|e| (e.target.clone(), e.params.clone()))
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
@@ -192,7 +211,7 @@ fn parse_and_probe(source: &str) -> Result<(Vec<Entry>, Probe), String> {
e.target.clone()
})
.collect();
let mut probe = probe_live_commands(&live_ids);
let mut probe = probe_live_commands(&live_calls);
for id in landed {
let mut s = gridcraft_engine::Session::new();
s.new_workbook();
@@ -224,7 +243,9 @@ fn render_table(entries: &[Entry]) -> String {
let value = match e.kind.as_str() {
"tab" => format!("Entry::Tab({:?})", e.target),
"command" if e.pending => format!("Entry::Finish(Action::Pending({:?}))", e.target),
"command" if !e.params.is_empty() => format!("Entry::Finish(Action::CommandWith({:?}, {:?}))", e.target, e.params),
"command" => format!("Entry::Finish(Action::Command({:?}))", e.target),
"dialog" if !e.params.is_empty() => format!("Entry::Finish(Action::DialogWith({:?}, {:?}))", e.target, e.params),
"dialog" => format!("Entry::Finish(Action::Dialog({:?}))", e.target),
"control" => format!("Entry::Finish(Action::Control({:?}))", e.target),
"continue" => "Entry::Continue".to_string(),
@@ -285,7 +306,7 @@ fn render_tests(entries: &[Entry], probe: &Probe) -> String {
seq = e.seq,
)
}
"command" => match probe.get(&e.target).copied().unwrap_or(2) {
"command" => match probe.get(&e.probe_key()).copied().unwrap_or(2) {
0 => format!(
"sequence(&mut h, {seq:?}, false);\n assert!(h.state().session.journal.iter().any(|(id, _)| id == {id:?}), \"{seq} must journal {id}\");",
seq = e.seq,
+24 -3
View File
@@ -2,6 +2,9 @@
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt as _;
#[derive(Default, Clone, Copy)]
struct Counts {
files: usize,
@@ -10,7 +13,24 @@ struct Counts {
proptests: usize,
}
fn walk(dir: &Path, c: &mut Counts) {
fn walk(dir: &Path, c: &mut Counts, seen: &mut std::collections::HashSet<u64>, budget: &mut usize) {
if *budget == 0 {
// Hard stop for pathological trees: two directory symlinks pointing at each other
// would otherwise be walked exponentially.
return;
}
// Directory identities (dev+inode on unix) make a symlink to a visited directory a
// no-op, not another exponential branch of the scan.
#[cfg(unix)]
let id = std::fs::metadata(dir).ok().and_then(|m| m.dev().checked_mul(1 << 60).and_then(|d| d.checked_add(m.ino())));
#[cfg(not(unix))]
let id: Option<u64> = None;
if let Some(id) = id
&& !seen.insert(id)
{
return;
}
*budget -= 1;
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
@@ -18,10 +38,11 @@ fn walk(dir: &Path, c: &mut Counts) {
let p = e.path();
let name = e.file_name();
let name = name.to_string_lossy();
// A symlinked directory is descended only if its identity is new (checked above).
if p.is_dir() {
// Nested fuzz workspaces and build output are not part of the crate.
if name != "target" && name != "fuzz" && !name.starts_with('.') {
walk(&p, c);
walk(&p, c, seen, budget);
}
} else if name.ends_with(".rs")
&& let Ok(s) = std::fs::read_to_string(&p)
@@ -71,7 +92,7 @@ pub fn run(_root: &Path, exact: bool) -> Result<(), String> {
continue;
};
let mut c = Counts::default();
walk(&dir, &mut c);
walk(&dir, &mut c, &mut std::collections::HashSet::new(), &mut 10_000);
let ex = if exact { exact_tests(&name) } else { None };
rows.push((name, c, ex));
}