mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-31 06:16:51 -06:00
* Tests remove the temp files they write Every test run left about 22 folders and files in the system temp directory: xref, alias, template, insert, linetype, script and recent-file fixtures, plus the pathless screenshot folder. The PDFBOOK test also wrote its second book to `$TMPDIR/b.pdf`, because `a^b.pdf` splits into `<prefix>-a.pdf` and `b.pdf`, and then removed a file that was never written. Tests that write files now use a `TempDir` guard (`tests/support/temp_dir.rs`, shared by the engine's unit and integration tests and copied for the UI crate). It gives each test its own folder and removes it on drop, even when an assertion fails. The PDFBOOK test also checks that both books land in its folder. The five tests that write pathless screenshots hold a test-only lock, so the shared screenshot folder can be removed once empty without racing another test's write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012FFPi6hJVyaEKV4PyE2fKq * Pathless screenshot cleanup survives failed tests and writes The test lock's guard now empties and removes the per-process screenshot directory when dropped, so a test that panics no longer leaves files behind, and the per-test remove calls are gone. A failed PNG write or flush removes the partial file, since the reply carries no path for anyone else to clean up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
9.5 KiB
Rust
203 lines
9.5 KiB
Rust
//! Drawing templates (#552): SAVEAS to a .dwt, NEW/QNEW from a template file, and the default
|
|
//! template (QNEWTEMPLATE), and a layout saved as a template (LAYOUT SAveas). The io hooks here keep drawings in memory: a "file" holds the number
|
|
//! of the drawing written, so the test sees exactly what the engine hands the file formats.
|
|
|
|
use std::sync::Mutex;
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::cmd::file::{IoHooks, set_io};
|
|
use cadcraft_engine::doc::Drawing;
|
|
use serde_json::json;
|
|
|
|
#[path = "support/temp_dir.rs"]
|
|
mod temp_dir;
|
|
|
|
use temp_dir::TempDir;
|
|
|
|
static FILES: Mutex<Vec<(String, Drawing)>> = Mutex::new(Vec::new());
|
|
|
|
fn write(d: &Drawing, name: &str) -> Result<Vec<u8>, String> {
|
|
let mut files = FILES.lock().unwrap();
|
|
files.push((name.to_string(), d.clone()));
|
|
Ok(format!("TEST {}", files.len() - 1).into_bytes())
|
|
}
|
|
|
|
fn read(bytes: &[u8], _name: &str) -> Result<Drawing, String> {
|
|
let text = String::from_utf8_lossy(bytes);
|
|
let i: usize = text.strip_prefix("TEST ").and_then(|n| n.trim().parse().ok()).ok_or("not a drawing")?;
|
|
FILES.lock().unwrap().get(i).map(|f| f.1.clone()).ok_or_else(|| "no such drawing".into())
|
|
}
|
|
|
|
fn hooks() {
|
|
set_io(IoHooks { read, write, plot: None });
|
|
}
|
|
|
|
/// A drawing with an office layer and a title block circle, saved as a template.
|
|
fn office_template(s: &mut Session, dir: &TempDir, name: &str) -> String {
|
|
s.execute("layer.new", &json!({ "name": "A-WALL", "color": 3 })).unwrap();
|
|
s.execute("circle", &json!({ "center": [5, 5], "radius": 2 })).unwrap();
|
|
let p = dir.path_str(name);
|
|
s.execute("saveas", &json!({ "path": p })).unwrap();
|
|
p
|
|
}
|
|
|
|
#[test]
|
|
fn saveas_dwt_then_new_from_the_template() {
|
|
hooks();
|
|
let mut s = Session::new();
|
|
let dir = TempDir::new("templates");
|
|
let dwt = office_template(&mut s, &dir, "office.dwt");
|
|
// A template is saved like a drawing: the format sees the .dwt name, the drawing is saved.
|
|
assert!(FILES.lock().unwrap().iter().any(|(n, _)| n.ends_with("office.dwt")));
|
|
let st = s.state().unwrap();
|
|
assert!(!st.is_dirty());
|
|
assert_eq!(st.title, "office.dwt");
|
|
|
|
let r = s.execute("new", &json!({ "template": dwt })).unwrap();
|
|
let st = s.state().unwrap();
|
|
assert_eq!(r["title"], json!(st.title));
|
|
assert!(st.title.starts_with("Drawing"), "{}", st.title);
|
|
assert!(st.path.is_none(), "an untitled drawing: saving asks for a name");
|
|
assert!(!st.is_dirty());
|
|
assert!(st.doc.layers.iter().any(|l| l.name == "A-WALL"));
|
|
assert_eq!(st.doc.model.iter().count(), 1);
|
|
assert!(s.execute("qsave", &json!({})).is_err(), "QSAVE needs a name: the template stays as it is");
|
|
}
|
|
|
|
#[test]
|
|
fn qnewtemplate_is_the_default_for_new_and_qnew() {
|
|
hooks();
|
|
let mut s = Session::new();
|
|
let dir = TempDir::new("templates");
|
|
let dwt = office_template(&mut s, &dir, "default.dwt");
|
|
let docs = s.docs.len();
|
|
|
|
s.execute("setvar", &json!({ "name": "QNEWTEMPLATE", "value": dwt })).unwrap();
|
|
assert_eq!(s.execute("getvar", &json!({ "name": "qnewtemplate" })).unwrap()["value"], json!(dwt));
|
|
s.cmdline("qnew").unwrap();
|
|
assert_eq!(s.docs.len(), docs + 1);
|
|
assert!(s.doc().unwrap().layers.iter().any(|l| l.name == "A-WALL"), "QNEW starts from the default template");
|
|
s.execute("new", &json!({})).unwrap();
|
|
assert!(s.doc().unwrap().layers.iter().any(|l| l.name == "A-WALL"));
|
|
|
|
// Units asked for, or "." for no template: a blank drawing.
|
|
s.execute("new", &json!({ "metric": true })).unwrap();
|
|
assert!(s.doc().unwrap().layers.iter().all(|l| l.name != "A-WALL"));
|
|
assert_eq!(s.doc().unwrap().header.i64("MEASUREMENT", 0), 1);
|
|
s.execute("new", &json!({ "template": "." })).unwrap();
|
|
assert!(s.doc().unwrap().layers.iter().all(|l| l.name != "A-WALL"));
|
|
|
|
s.execute("setvar", &json!({ "name": "QNEWTEMPLATE", "value": "." })).unwrap();
|
|
assert_eq!(s.execute("getvar", &json!({ "name": "QNEWTEMPLATE" })).unwrap()["value"], json!(""));
|
|
s.execute("new", &json!({})).unwrap();
|
|
assert!(s.doc().unwrap().layers.iter().all(|l| l.name != "A-WALL"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_template_that_cannot_be_read_makes_no_drawing() {
|
|
hooks();
|
|
let mut s = Session::new();
|
|
let docs = s.docs.len();
|
|
let err = s.execute("new", &json!({ "template": "/no/such/folder/office.dwt" })).unwrap_err().to_string();
|
|
assert!(err.contains("office.dwt"), "{err}");
|
|
let dir = TempDir::new("templates");
|
|
let junk = dir.file("junk.dwt", b"not a drawing at all");
|
|
assert!(s.execute("new", &json!({ "template": junk })).is_err());
|
|
assert!(s.execute("new", &json!({ "template": 42 })).is_err());
|
|
assert_eq!(s.docs.len(), docs);
|
|
}
|
|
|
|
/// The drawing the io hooks were last handed for a file whose name ends with `name`.
|
|
fn written(name: &str) -> Drawing {
|
|
FILES.lock().unwrap().iter().rev().find(|(n, _)| n.ends_with(name)).map(|f| f.1.clone()).unwrap()
|
|
}
|
|
|
|
fn layout_names(d: &Drawing) -> Vec<String> {
|
|
d.layouts.iter().map(|l| l.name.clone()).collect()
|
|
}
|
|
|
|
/// LAYOUT SAveas: the template holds the layout alone, with the named objects it uses, and the
|
|
/// drawing it came from keeps everything.
|
|
#[test]
|
|
fn layout_saveas_writes_the_layout_and_the_named_objects_it_uses() {
|
|
hooks();
|
|
let mut s = Session::new();
|
|
let dir = TempDir::new("templates");
|
|
for name in ["TITLE", "PARTS-LAYER", "UNUSED", "WALLS", "NOTES"] {
|
|
s.execute("layer.new", &json!({ "name": name })).unwrap();
|
|
}
|
|
s.execute("layer.current", &json!({ "name": "PARTS-LAYER" })).unwrap();
|
|
let c = s.execute("circle", &json!({ "center": [5, 5], "radius": 2 })).unwrap()["handle"].clone();
|
|
s.execute("block", &json!({ "name": "PARTS", "base": [0, 0], "handles": [c], "keep": "retain" })).unwrap();
|
|
// WALLS has model space objects only, and NOTES none; the sheet's viewport freezes one and recolors the other.
|
|
s.execute("layer.current", &json!({ "name": "WALLS" })).unwrap();
|
|
s.execute("line", &json!({ "points": [[0, 0], [0, 10]] })).unwrap();
|
|
let vp = s.execute("layout.new", &json!({ "name": "Sheet" })).unwrap()["viewport"].clone();
|
|
s.execute("viewport.set", &json!({ "handle": vp, "freeze": ["WALLS"], "colors": { "NOTES": 1 } })).unwrap();
|
|
s.execute("layout.set", &json!({ "name": "Sheet" })).unwrap();
|
|
s.execute("layer.current", &json!({ "name": "TITLE" })).unwrap();
|
|
s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap();
|
|
s.execute("layer.current", &json!({ "name": "0" })).unwrap();
|
|
let sheet_objects = s.doc().unwrap().layout("Sheet").unwrap().entities.iter().count();
|
|
|
|
let p = dir.path_str("sheet.dwt");
|
|
let r = s.execute("layout.saveas", &json!({ "path": p })).unwrap();
|
|
assert_eq!(r["name"], json!("Sheet"), "the current layout by default");
|
|
assert_eq!(r["path"], json!(p));
|
|
let t = written("sheet.dwt");
|
|
assert_eq!(layout_names(&t), ["Sheet"]);
|
|
assert_eq!(t.active_layout.as_deref(), Some("Sheet"));
|
|
assert_eq!(t.model.iter().count(), 0, "no model space objects");
|
|
assert_eq!(t.layout("Sheet").unwrap().entities.iter().count(), sheet_objects, "the layout's viewport and line");
|
|
assert!(t.layer("TITLE").is_some() && t.layer("0").is_some());
|
|
assert!(t.layer("UNUSED").is_none() && t.layer("PARTS-LAYER").is_none(), "unused layers are left out");
|
|
assert!(t.block("PARTS").is_none(), "unused blocks are left out");
|
|
// Layers the viewport freezes or recolors stay, and so do those settings.
|
|
assert!(t.layer("WALLS").is_some() && t.layer("NOTES").is_some());
|
|
let sheet_vp = t.layout("Sheet").unwrap().entities.iter().find_map(|e| match &e.kind {
|
|
cadcraft_engine::doc::EntityKind::Viewport(v) if v.id != 1 => Some(v.clone()),
|
|
_ => None,
|
|
});
|
|
let sheet_vp = sheet_vp.unwrap();
|
|
assert_eq!(sheet_vp.frozen_layers, ["WALLS"]);
|
|
assert_eq!(sheet_vp.layer_colors.len(), 1);
|
|
|
|
// The drawing is not changed.
|
|
let d = s.doc().unwrap();
|
|
assert_eq!(layout_names(d), ["Layout1", "Layout2", "Sheet"]);
|
|
assert_eq!(d.model.iter().count(), 2);
|
|
assert!(d.layer("UNUSED").is_some() && d.block("PARTS").is_some());
|
|
|
|
// A drawing started from the template has the layout.
|
|
s.execute("new", &json!({ "template": p })).unwrap();
|
|
assert_eq!(layout_names(s.doc().unwrap()), ["Sheet"]);
|
|
}
|
|
|
|
#[test]
|
|
fn typed_layout_saveas_asks_for_the_layout_and_the_template_file() {
|
|
hooks();
|
|
let mut s = Session::new();
|
|
let dir = TempDir::new("templates");
|
|
s.cmdline("LAYOUT").unwrap();
|
|
s.cmdline("sa").unwrap();
|
|
assert_eq!(s.current_prompt().unwrap().display(), "Enter layout to save to template <Layout1>:");
|
|
s.cmdline("Layout2").unwrap();
|
|
assert_eq!(s.current_prompt().unwrap().display(), "Enter template file name <Layout2.dwt>:");
|
|
// No extension: the template is a .dwt.
|
|
let p = dir.path_str("typed");
|
|
s.cmdline(&p).unwrap();
|
|
assert!(s.current_prompt().is_none());
|
|
let saved = format!("{p}.dwt");
|
|
assert!(s.log.iter().any(|l| *l == format!("Layout \"Layout2\" saved to template \"{saved}\".")), "{:?}", s.log);
|
|
assert_eq!(layout_names(&written("typed.dwt")), ["Layout2"]);
|
|
|
|
// The JSON form of LAYOUT, other file types, a missing path and an unknown layout.
|
|
let dxf = dir.path_str("sheet1.dxf");
|
|
let r = s.execute("layout", &json!({ "option": "saveas", "name": "layout1", "path": dxf })).unwrap();
|
|
assert_eq!(r["name"], json!("Layout1"));
|
|
assert_eq!(layout_names(&written("sheet1.dxf")), ["Layout1"]);
|
|
assert!(s.execute("layout.saveas", &json!({ "path": dir.path_str("sheet.png") })).is_err());
|
|
assert!(s.execute("layout.saveas", &json!({})).is_err());
|
|
assert!(s.execute("layout.saveas", &json!({ "name": "Nope", "path": dir.path_str("nope.dwt") })).is_err());
|
|
}
|