mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46:51 -06:00
* cli: act on a handle an earlier --cmd returned ({last}) (#1663)
A run's --cmd steps were independent: mview answered {"viewports":["100"]}
but viewport.set could not use that handle, so it failed with "no viewport
given".
The batch runner now remembers the last object handle a --cmd returned (the
strings in its result that name an entity of the active drawing, so counts and
names are not mistaken for handles). A parameter value of exactly {last} in a
later --cmd, at any depth, is replaced with it; every other value is passed
through unchanged. With no handle yet the run fails with a clear error instead
of passing a bad handle to the command.
Fixes #1663
* cli: keep {last} to the drawing and handle fields that returned it
Read handles only from result handle fields, never arbitrary strings: a group named like another object's handle is a name, not a reference. Clear the remembered handle when new, open, document.switch (or a script running one) makes another drawing current, and keep a handle the new drawing's own command returns.
226 lines
9.3 KiB
Rust
226 lines
9.3 KiB
Rust
//! A `--cmd` can act on a handle an earlier `--cmd` in the same run returned (#1663).
|
|
//!
|
|
//! `mview` answers `{"viewports":["100"]}`, but a following `viewport.set` could not use that
|
|
//! handle: it had to be guessed, selected by point, or activated with `mspace`. A `--cmd` parameter
|
|
//! value of exactly `{last}` is now filled in with the last handle an earlier `--cmd` returned.
|
|
|
|
use std::process::{Command, Output};
|
|
|
|
fn run(args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_cadcraft-cli"))
|
|
.args(args)
|
|
.stdout(std::process::Stdio::piped())
|
|
.stderr(std::process::Stdio::piped())
|
|
.output()
|
|
.expect("run cadcraft-cli")
|
|
}
|
|
|
|
/// The issue's sequence: `viewport.set` straight after `mview`, addressing the new viewport by the
|
|
/// handle the runner remembers.
|
|
#[test]
|
|
fn viewport_set_after_mview_uses_the_returned_handle() {
|
|
let out = run(&[
|
|
"run",
|
|
"--metric",
|
|
"--cmd",
|
|
r#"layout.set {"name":"Layout1"}"#,
|
|
"--cmd",
|
|
r#"mview {"p1":[20,20],"p2":[200,150]}"#,
|
|
"--cmd",
|
|
r#"viewport.set {"handle":"{last}","scale":1}"#,
|
|
]);
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(out.status.success(), "exit {:?}\nstdout: {stdout}\nstderr: {stderr}", out.status);
|
|
let mview = stdout.lines().find(|l| l.contains("\"viewports\"")).unwrap_or_else(|| panic!("no mview result in {stdout:?}"));
|
|
let created: serde_json::Value = serde_json::from_str(mview).expect("mview result is JSON");
|
|
let handle = created["viewports"].as_array().and_then(|a| a.first()).and_then(|h| h.as_str()).expect("a viewport handle");
|
|
assert!(handle.eq_ignore_ascii_case("100"), "{mview}");
|
|
let set = stdout.lines().find(|l| l.contains("\"viewHeight\"")).unwrap_or_else(|| panic!("no viewport.set result in {stdout:?}"));
|
|
let vp: serde_json::Value = serde_json::from_str(set).expect("viewport.set result is JSON");
|
|
assert_eq!(vp[0]["handle"].as_str(), Some(handle), "viewport.set changed the viewport `{handle}`: {set}");
|
|
assert_eq!(vp[0]["scale"].as_f64(), Some(1.0), "{set}");
|
|
}
|
|
|
|
/// `{last}` with no handle to fill in is a clear error, not the token reaching the command.
|
|
#[test]
|
|
fn last_without_an_earlier_handle_is_an_error() {
|
|
let out = run(&["run", "--metric", "--cmd", r#"viewport.set {"handle":"{last}","scale":1}"#]);
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert_eq!(out.status.code(), Some(1), "stdout: {:?}\nstderr: {stderr}", String::from_utf8_lossy(&out.stdout));
|
|
assert!(stderr.contains("{last}"), "the error names the placeholder: {stderr}");
|
|
assert!(!stderr.contains("panicked"), "{stderr}");
|
|
}
|
|
|
|
/// The last JSON value a run printed: command results are one JSON value a line (`null` is not
|
|
/// printed).
|
|
fn last_json(stdout: &str) -> serde_json::Value {
|
|
let line = stdout
|
|
.lines()
|
|
.rev()
|
|
.find(|l| {
|
|
let t = l.trim_start();
|
|
t.starts_with('{') || t.starts_with('[')
|
|
})
|
|
.unwrap_or_else(|| panic!("no JSON result in {stdout:?}"));
|
|
serde_json::from_str(line).expect("command result is JSON")
|
|
}
|
|
|
|
/// `{last}` was not replaced: the error names the placeholder, and the run fails cleanly.
|
|
fn assert_needs_an_earlier_handle(out: &Output) {
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert_eq!(out.status.code(), Some(1), "stdout: {:?}\nstderr: {stderr}", String::from_utf8_lossy(&out.stdout));
|
|
assert!(stderr.contains("{last}"), "the error names the placeholder: {stderr}");
|
|
assert!(!stderr.contains("panicked"), "{stderr}");
|
|
}
|
|
|
|
/// A user-defined name that happens to look like a handle is not a returned handle. `group`
|
|
/// answers `{"name":"100","members":1}`: `100` is the first line's handle, but the name is not an
|
|
/// object reference and must not replace the last handle the `line` command returned (`101`).
|
|
#[test]
|
|
fn a_group_name_that_looks_like_a_handle_is_not_last() {
|
|
let out = run(&[
|
|
"run",
|
|
"--metric",
|
|
"--cmd",
|
|
r#"line {"points":[[0,0],[10,0]]}"#,
|
|
"--cmd",
|
|
r#"line {"points":[[0,10],[10,10]]}"#,
|
|
"--cmd",
|
|
r#"group {"name":"100","handles":["101"]}"#,
|
|
"--cmd",
|
|
r#"erase {"handles":["{last}"]}"#,
|
|
"--cmd",
|
|
r#"entities {"type":"LINE"}"#,
|
|
]);
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(out.status.success(), "exit {:?}\nstdout: {stdout}\nstderr: {stderr}", out.status);
|
|
let r = last_json(&stdout);
|
|
assert_eq!(r["count"].as_u64(), Some(1), "one line is left: {stdout}");
|
|
let left: Vec<&str> = r["entities"].as_array().into_iter().flatten().filter_map(|e| e["handle"].as_str()).collect();
|
|
assert_eq!(left, ["100"], "the group name 100 did not replace the handle 101 the line command returned: {stdout}");
|
|
}
|
|
|
|
/// `new` makes another drawing current: the handle an earlier `--cmd` returned in the old drawing
|
|
/// is not a handle of the new one.
|
|
#[test]
|
|
fn last_is_cleared_when_new_switches_drawings() {
|
|
let out = run(&[
|
|
"run",
|
|
"--metric",
|
|
"--cmd",
|
|
r#"line {"points":[[0,0],[10,0]]}"#,
|
|
"--cmd",
|
|
r#"new {"metric":true}"#,
|
|
"--cmd",
|
|
r#"erase {"handles":["{last}"]}"#,
|
|
]);
|
|
assert_needs_an_earlier_handle(&out);
|
|
}
|
|
|
|
/// `open` makes the opened file the current drawing: a handle from the drawing before it is stale.
|
|
#[test]
|
|
fn last_is_cleared_when_open_switches_drawings() {
|
|
let dir = std::env::temp_dir().join(format!("cc-cli-last-open-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).expect("temp dir");
|
|
let dxf = dir.join("opened.dxf");
|
|
let sample = run(&["sample", "bracket", dxf.to_str().expect("temp path")]);
|
|
assert!(sample.status.success(), "sample failed: {}", String::from_utf8_lossy(&sample.stderr));
|
|
let open = format!(r#"open {{"path":{}}}"#, serde_json::to_string(dxf.to_str().expect("temp path")).expect("path JSON"));
|
|
let out = run(&["run", "--metric", "--cmd", r#"line {"points":[[0,0],[10,0]]}"#, "--cmd", &open, "--cmd", r#"erase {"handles":["{last}"]}"#]);
|
|
assert_needs_an_earlier_handle(&out);
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
/// `document.switch` changes the current drawing without adding one: a handle remembered from the
|
|
/// drawing left behind must not be used in the one switched to.
|
|
#[test]
|
|
fn last_is_cleared_when_document_switch_changes_drawings() {
|
|
let out = run(&[
|
|
"run",
|
|
"--metric",
|
|
"--cmd",
|
|
r#"line {"points":[[0,0],[10,0]]}"#,
|
|
"--cmd",
|
|
r#"new {"metric":true}"#,
|
|
"--cmd",
|
|
r#"line {"points":[[0,10],[10,10]]}"#,
|
|
"--cmd",
|
|
r#"document.switch {"index":0}"#,
|
|
"--cmd",
|
|
r#"erase {"handles":["{last}"]}"#,
|
|
]);
|
|
assert_needs_an_earlier_handle(&out);
|
|
}
|
|
|
|
/// A handle a command returns after a switch belongs to the drawing now current, so it is kept.
|
|
#[test]
|
|
fn a_handle_returned_after_a_switch_is_last() {
|
|
let out = run(&[
|
|
"run",
|
|
"--metric",
|
|
"--cmd",
|
|
r#"line {"points":[[0,0],[10,0]]}"#,
|
|
"--cmd",
|
|
r#"new {"metric":true}"#,
|
|
"--cmd",
|
|
r#"document.switch {"index":0}"#,
|
|
"--cmd",
|
|
r#"line {"points":[[0,10],[10,10]]}"#,
|
|
"--cmd",
|
|
r#"erase {"handles":["{last}"]}"#,
|
|
"--cmd",
|
|
r#"entities {"type":"LINE"}"#,
|
|
]);
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(out.status.success(), "exit {:?}\nstdout: {stdout}\nstderr: {stderr}", out.status);
|
|
let r = last_json(&stdout);
|
|
assert_eq!(r["count"].as_u64(), Some(1), "one line is left: {stdout}");
|
|
let left: Vec<&str> = r["entities"].as_array().into_iter().flatten().filter_map(|e| e["handle"].as_str()).collect();
|
|
assert_eq!(left, ["100"], "the line created after the switch was erased: {stdout}");
|
|
}
|
|
|
|
/// A script can change the current drawing too (`new {"metric":true}` at a script line), so the
|
|
/// remembered handle is cleared after a script that changed it.
|
|
#[test]
|
|
fn last_is_cleared_by_a_script_that_changes_drawings() {
|
|
let out = run(&[
|
|
"run",
|
|
"--metric",
|
|
"--cmd",
|
|
r#"line {"points":[[0,0],[10,0]]}"#,
|
|
"--script",
|
|
r#"new {"metric":true}"#,
|
|
"--cmd",
|
|
r#"erase {"handles":["{last}"]}"#,
|
|
]);
|
|
assert_needs_an_earlier_handle(&out);
|
|
}
|
|
|
|
/// Only the exact token is a placeholder: ordinary JSON strings are left alone.
|
|
#[test]
|
|
fn ordinary_values_are_not_placeholders() {
|
|
let dir = std::env::temp_dir().join(format!("cc-cli-last-placeholder-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).expect("temp dir");
|
|
let out_dxf = dir.join("text.dxf");
|
|
let out = run(&[
|
|
"run",
|
|
"--metric",
|
|
"--cmd",
|
|
r#"layout.set {"name":"Layout1"}"#,
|
|
"--cmd",
|
|
r#"mview {"p1":[20,20],"p2":[200,150]}"#,
|
|
"--cmd",
|
|
r#"text {"at":[0,0],"height":2,"text":"last and {last}"}"#,
|
|
"--save",
|
|
out_dxf.to_str().expect("temp path"),
|
|
]);
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(out.status.success(), "exit {:?}\nstderr: {stderr}", out.status);
|
|
let saved = std::fs::read_to_string(&out_dxf).expect("saved DXF");
|
|
assert!(saved.contains("last and {last}"), "the text is stored literally");
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|