soundcraft/crates/ui-egui/tests/command_reachability.rs

111 lines
3.9 KiB
Rust

//! Every user-facing engine command must be reachable from the GUI: through a menu, a shortcut,
//! or a widget that names it in `src/`. Commands that only work from the control channel / CLI /
//! MCP left users stuck (#282: `midi.new_clip` had no UI path, so an empty instrument track could
//! never get a MIDI clip).
use soundcraft_engine::{catalog, cmd::command_specs};
use soundcraft_ui_egui::menus::{self, MenuNode};
use std::collections::HashSet;
use std::path::Path;
/// Known gaps: commands with no menu, shortcut or literal reference in the UI source today.
/// Give each one a UI path and remove it from this list; don't add to it.
const KNOWN_UNREACHED: &[&str] = &[
"automation.clear",
"edit.paste_clip_effects",
"edit.paste_clip_gain",
"event.midi_note",
"file.bounce_stems",
"markers.next",
"markers.previous",
"mix.clear_mutes",
"mix.clear_solos",
"mix.insert_move",
"mix.instrument_param",
"mix.phase_invert",
"mix.sampler_clear",
"mix.sampler_load",
"mix.send_surround_pan",
"mix.solo_safe",
"options.post_roll",
"options.pre_roll",
"setup.hardware", // menu opens the Playback Engine window, which doesn't run it
"setup.playback_engine", // same
"setup.reset_edit_state",
"track.elastic",
"track.load_preset",
"track.pin",
"track.timebase",
"track.trim_automation_write",
"track.vca",
"transport.pause",
"transport.status",
"video.offset",
"view.sub_counter",
];
fn ui_source() -> String {
fn walk(dir: &Path, out: &mut String) {
for entry in std::fs::read_dir(dir).unwrap() {
let p = entry.unwrap().path();
if p.is_dir() {
walk(&p, out);
} else if p.extension().is_some_and(|e| e == "rs") {
out.push_str(&std::fs::read_to_string(&p).unwrap());
}
}
}
let mut s = String::new();
walk(&Path::new(env!("CARGO_MANIFEST_DIR")).join("src"), &mut s);
s
}
/// Command ids behind the leaves of the menu bar actually built by the app. A spec's own `menu`
/// field is not enough: the bar is built from the catalog, so an off-catalog path never shows.
fn menu_commands() -> HashSet<String> {
fn walk(nodes: &[MenuNode], extra: &[(&str, &str)], out: &mut HashSet<String>) {
for n in nodes {
if n.children.is_empty() {
out.extend(catalog::implemented_by(&n.path, extra));
}
walk(&n.children, extra, out);
}
}
let mut out = HashSet::new();
walk(&menus::tree(), &menus::ui_aliases(), &mut out);
out
}
#[test]
fn every_command_has_a_ui_path() {
let src = ui_source();
let in_menus = menu_commands();
let mut unreached = Vec::new();
let mut stale = Vec::new();
for c in command_specs() {
// Queries and view-only commands are for agents / the UI's own reads.
if !c.journal || c.view {
continue;
}
let reached = in_menus.contains(c.id) || c.shortcut.is_some() || src.contains(&format!("\"{}\"", c.id));
let known = KNOWN_UNREACHED.contains(&c.id);
if !reached && !known {
unreached.push(c.id);
} else if reached && known {
stale.push(c.id);
}
}
assert!(unreached.is_empty(), "commands with no menu, shortcut or UI reference: {unreached:?}");
assert!(stale.is_empty(), "now reachable, remove from KNOWN_UNREACHED: {stale:?}");
}
#[test]
fn new_midi_clip_is_in_the_clip_menu() {
let extra = menus::ui_aliases();
assert_eq!(catalog::implemented_by("Clip > New MIDI Clip", &extra).as_deref(), Some("midi.new_clip"));
assert!(menu_commands().contains("midi.new_clip"));
let tree = menus::tree();
let clip = tree.iter().find(|n| n.label == "Clip").unwrap();
assert_eq!(clip.children[0].label, "New MIDI Clip", "creates rather than edits, so it comes first");
}