Files
cadcraft/crates/engine/tests/it/query_entities.rs
aiongg 9cf115e808 CI: one test binary per crate, nextest, line tables only (#1989)
* Build: line tables only in dev, no debug info for dependencies

* Tests: one integration-test binary per crate

Each crate's tests/*.rs becomes a module of tests/it/main.rs. Files that
install process-wide state (the file io hooks, user aliases, SAVETIME and
SAVEFILEPATH, user linetype and pattern files, registered fonts) stay their
own test binaries.

* xtask ci: nextest, tests without examples, one wasm check call
2026-10-23 13:34:26 -04:00

105 lines
5.4 KiB
Rust

//! The `entities` query (control channel `engine.execute`, MCP `query_entities`) rejects malformed
//! filters instead of ignoring them and returning everything.
use cadcraft_engine::Session;
use serde_json::json;
#[test]
fn malformed_filters_are_errors() {
let mut s = Session::new();
s.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap();
s.execute("circle", &json!({"center": [100, 100], "radius": 1})).unwrap();
for p in [
json!({"window": [-5, -5, 5, 5]}),
json!({"window": [[-5, -5]]}),
json!({"window": [[-5, -5], [5, 5], [9, 9]]}),
json!({"window": {"min": [-5, -5], "max": [5, 5]}}),
json!({"window": "x"}),
json!({"type": 7}),
json!({"layer": ["0"]}),
json!({"limit": -1}),
json!({"offset": "1"}),
json!({"crossing": "yes"}),
json!({"selected": "true"}),
json!({"selected": 1}),
json!({"selected": []}),
json!({"selected": {}}),
] {
assert!(s.execute("entities", &p).is_err(), "{p} must be rejected");
}
let r = s.execute("entities", &json!({"window": [[-5, -5], [5, 5]]})).unwrap();
assert_eq!(r["count"], 1, "{r}");
}
#[test]
fn selection_filter_combines_before_paging_without_changing_state() {
let mut s = Session::new();
s.execute("layer.new", &json!({"name": "측정", "current": true})).unwrap();
for x in [0, 10, 20] {
s.execute("circle", &json!({"center": [x, 0], "radius": 2})).unwrap();
}
s.execute("line", &json!({"points": [[0, 0], [3, 4]]})).unwrap();
let all = s.execute("entities", &json!({})).unwrap();
let hs: Vec<_> = all["entities"].as_array().unwrap().iter().map(|e| e["handle"].clone()).collect();
// Leave a redo entry to prove the query doesn't clear redo or create an undo entry.
s.execute("circle", &json!({"center": [99, 99], "radius": 1})).unwrap();
s.undo().unwrap();
s.execute("select", &json!({"handles": [hs[0], hs[2], hs[3]]})).unwrap();
let before = s.state().unwrap().clone();
let report = s.execute("drawing.inspect", &json!({})).unwrap();
let selected = s.execute("entities", &json!({"selected": true})).unwrap();
assert_eq!(selected["count"], 3);
assert_eq!(selected["entities"][0]["handle"], hs[0]);
assert_eq!(selected["entities"][1]["handle"], hs[2]);
assert_eq!(s.execute("entities", &json!({"selected": false})).unwrap(), all);
assert_eq!(s.execute("entities", &json!({"selected": null})).unwrap(), all);
assert_eq!(s.execute("entities", &json!({})).unwrap(), all);
let page = s.execute("entities", &json!({"selected": true, "type": "circle", "layer": "측정", "limit": 1, "offset": 1})).unwrap();
assert_eq!(page, json!({"count": 2, "returned": 1, "offset": 1, "entities": [all["entities"][2]]}));
let window = json!({"selected": true, "type": "circle", "layer": "측정", "window": [[-3, -3], [3, 3]], "crossing": false});
assert_eq!(s.execute("entities", &window).unwrap()["count"], 1);
assert_eq!(s.execute("entities", &json!({"selected": true, "layer": "other"})).unwrap()["count"], 0);
assert_eq!(s.execute("entities", &json!({"selected": true, "limit": 0})).unwrap()["returned"], 0);
assert_eq!(s.execute("entities", &json!({"selected": true, "offset": 99})).unwrap()["returned"], 0);
assert!(s.execute("entities", &json!({"selected": "true"})).is_err());
assert_eq!(s.execute("drawing.inspect", &json!({})).unwrap(), report);
let after = s.state().unwrap();
assert!(std::sync::Arc::ptr_eq(&before.doc, &after.doc));
assert!(std::sync::Arc::ptr_eq(&before.saved, &after.saved));
assert_eq!(before.revision, after.revision);
assert_eq!(before.selection, after.selection);
assert_eq!(before.previous_selection, after.previous_selection);
assert_eq!(before.is_dirty(), after.is_dirty());
for (a, b) in [(&before.undo, &after.undo), (&before.redo, &after.redo)] {
assert_eq!(a.len(), b.len());
for (a, b) in a.iter().zip(b) {
assert_eq!(a.label, b.label);
assert_eq!(a.selection, b.selection);
assert!(std::sync::Arc::ptr_eq(&a.doc, &b.doc));
}
}
}
#[test]
fn selection_is_empty_by_default_and_limited_to_active_space() {
use cadcraft_doc::{Handle, Space};
let mut s = Session::new();
s.execute("circle", &json!({"center": [0, 0], "radius": 2})).unwrap();
let model = s.doc().unwrap().model.iter().next().unwrap().handle;
assert_eq!(s.execute("entities", &json!({"selected": true})).unwrap()["count"], 0);
s.execute("layout.set", &json!({"name": "Layout1"})).unwrap();
s.execute("circle", &json!({"center": [10, 10], "radius": 2})).unwrap();
let paper = s.doc().unwrap().space(&Space::Paper("Layout1".into())).unwrap().iter().last().unwrap().handle;
// Even stale, duplicate and foreign-space selection handles cannot leak entities.
s.state_mut().unwrap().selection = vec![model, paper, paper, Handle(u64::MAX)];
let page = s.execute("entities", &json!({"selected": true})).unwrap();
assert_eq!(page["count"], 1);
assert_eq!(page["entities"][0]["handle"], paper.hex());
s.state_mut().unwrap().space = Space::Model;
let page = s.execute("entities", &json!({"selected": true})).unwrap();
assert_eq!(page["count"], 1);
assert_eq!(page["entities"][0]["handle"], model.hex());
s.state_mut().unwrap().selection.clear();
assert_eq!(s.execute("entities", &json!({"selected": true})).unwrap()["entities"], json!([]));
}