Merge PR #351: make BATTMAN work like ATTMAN (#295)

External PR by Kevin Griffin (@KevinGriffin-new).
The ATTMAN/BATTMAN arm stripped the argument with
trim_start_matches("ATTMAN"), which left "BATTMAN" intact as a bogus
block-name filter, so BATTMAN always reported 'no matching block'.
Strip whichever keyword was actually typed (BATTMAN first, since ATTMAN
is a suffix of it) and match the 'BATTMAN ' prefix form. Adds two
regression tests driving the real command dispatcher.
This commit is contained in:
Hakan Seven committed 2026-07-11 09:28:48 +03:00
commit bd6a8845b4
2 files changed
+103 -3

No files matched your search

+1 -1
View File
@@ -194,7 +194,7 @@ Status of every standard CAD command in Open CAD Studio:
| `ATTEXT` | — | Extract attributes (legacy) | ✅ |
| `ATTSYNC` | — | Synchronize attribute definitions | ✅ |
| `BLOCKPALETTE` | — | Multi-view block palette (command-line list) | 🔶 |
| `ATTMAN` | — | Attribute manager (command-line list) | 🔶 |
| `ATTMAN` | `BATTMAN` | Attribute manager (command-line list) | 🔶 |
| `XBIND` | XB | Bind xref elements to drawing | ❌ |
| `XOPEN` | — | Open xref for editing | ✅ |
| `BSAVE` | — | Save block in editor | ❌ |
+102 -2
View File
@@ -430,8 +430,19 @@ impl OpenCADStudio {
// ATTMAN — list the attribute definitions of each block (or one named
// block). The full graphical manager is not built; this reports the
// same information from the command line.
cmd if cmd == "ATTMAN" || cmd == "BATTMAN" || cmd.starts_with("ATTMAN ") => {
let arg = cmd.trim_start_matches("ATTMAN").trim();
cmd if cmd == "ATTMAN"
|| cmd == "BATTMAN"
|| cmd.starts_with("ATTMAN ")
|| cmd.starts_with("BATTMAN ") =>
{
// BATTMAN is an alias for ATTMAN. Strip whichever keyword was
// typed so the remainder is the optional block-name argument;
// check BATTMAN first since ATTMAN is a suffix of it.
let arg = cmd
.strip_prefix("BATTMAN")
.or_else(|| cmd.strip_prefix("ATTMAN"))
.unwrap_or("")
.trim();
let blocks = self.tabs[i].scene.custom_block_names();
let targets: Vec<String> = if arg.is_empty() {
blocks
@@ -637,3 +648,92 @@ impl OpenCADStudio {
Some(self.finish_dispatch(cmd))
}
}
#[cfg(test)]
mod tests {
use crate::app::OpenCADStudio;
/// Build a headless app with a single custom block "MYBLK" carrying one
/// attribute definition (COST=0), so ATTMAN/BATTMAN have something to list.
fn app_with_attributed_block() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
let doc = &mut app.tabs[i].scene.document;
let br_h = acadrust::Handle::new(doc.next_handle());
let mut br = acadrust::tables::BlockRecord::new("MYBLK");
br.handle = br_h;
doc.block_records.add(br).unwrap();
let mut ad = acadrust::entities::AttributeDefinition {
tag: "COST".into(),
prompt: "Cost".into(),
default_value: "0".into(),
insertion_point: acadrust::types::Vector3::new(0.0, 0.0, 0.0),
height: 1.0,
..Default::default()
};
ad.common.owner_handle = br_h;
doc.add_entity(acadrust::EntityType::AttributeDefinition(ad))
.unwrap();
app
}
/// Run one command line and return only the command-line text it appended.
fn run_capture(app: &mut OpenCADStudio, cmd: &str) -> String {
let start = app.command_line.history.len();
let _ = app.run_command_line(cmd);
app.command_line.history[start..]
.iter()
.map(|e| e.text.as_str())
.collect::<Vec<_>>()
.join("\n")
}
// Regression for #295: BATTMAN triggered the ATTMAN handler but the argument
// was extracted with `trim_start_matches("ATTMAN")`, which leaves "BATTMAN"
// as a bogus block-name argument — so BATTMAN always reported
// "ATTMAN: no matching block" instead of listing attributes like ATTMAN.
#[test]
fn battman_is_a_working_alias_for_attman() {
let attman_out = run_capture(&mut app_with_attributed_block(), "ATTMAN");
let battman_out = run_capture(&mut app_with_attributed_block(), "BATTMAN");
// ATTMAN lists the block; the reported bug is that BATTMAN did not.
assert!(
attman_out.contains("MYBLK"),
"ATTMAN should list the block, got: {attman_out:?}"
);
assert!(
battman_out.contains("MYBLK"),
"BATTMAN should list the block like ATTMAN, got: {battman_out:?}"
);
assert!(
!battman_out.contains("no matching block"),
"BATTMAN must not fall into the no-argument error path, got: {battman_out:?}"
);
// The alias must behave identically, attribute listing included.
assert_eq!(
attman_out, battman_out,
"BATTMAN output must match ATTMAN output exactly"
);
}
// The keyword-stripping fix must also honour an explicit block-name argument
// on BATTMAN (`BATTMAN <name>`), which the old code never matched at all.
#[test]
fn battman_honours_a_block_name_argument() {
let hit = run_capture(&mut app_with_attributed_block(), "BATTMAN MYBLK");
assert!(
hit.contains("MYBLK") && !hit.contains("no matching block"),
"BATTMAN MYBLK should list that block, got: {hit:?}"
);
let miss = run_capture(&mut app_with_attributed_block(), "BATTMAN NOPE");
assert!(
miss.contains("no matching block"),
"BATTMAN NOPE should report no match, got: {miss:?}"
);
}
}