Files
cadcraft/crates/engine/tests/insert_file.rs
T
1d987904f9 Tests remove the temp files they write (#2232)
* 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>
2026-10-29 21:21:54 -04:00

175 lines
7.6 KiB
Rust

//! INSERT of exploded blocks (`*NAME`) and of drawing files (`-INSERT path`, `NAME=path`, JSON
//! `file`), read through the io hooks (#340).
use cadcraft_engine::Session;
use cadcraft_engine::cmd::file::{IoHooks, set_io};
use cadcraft_engine::doc::color::Color;
use cadcraft_engine::doc::geom::Vec3;
use cadcraft_engine::doc::{Block, Circle, Drawing, Entity, EntityKind, HVal, Handle, Insert, Layer, Line};
use serde_json::json;
#[path = "support/temp_dir.rs"]
mod temp_dir;
use temp_dir::TempDir;
/// A drawing file in a test format: its bytes are "x,y", the $INSBASE. Model space: a line from
/// 10,10 to 20,10 on layer PARTS (colour 1), and a reference to block NUT (a circle) at 10,10.
fn read(bytes: &[u8], _name: &str) -> Result<Drawing, String> {
let text = String::from_utf8_lossy(bytes);
let (x, y) = text.trim().split_once(',').ok_or("bad test file")?;
let (x, y): (f64, f64) = (x.parse().map_err(|_| "x")?, y.parse().map_err(|_| "y")?);
let mut d = Drawing::new_imperial();
d.header.set("INSBASE", HVal::Point(Vec3::new(x, y, 0.0)));
let mut parts = Layer::new("PARTS");
parts.color = Color::Index(1);
d.layers.push(parts);
let mut nut = Block::new("NUT");
nut.entities.push(Entity::new(Handle(0x50), EntityKind::Circle(Circle { center: Vec3::ZERO, radius: 1.0 })));
d.blocks.insert("NUT".into(), std::sync::Arc::new(nut));
let mut line = Entity::new(Handle(0x60), EntityKind::Line(Line { a: Vec3::new(10.0, 10.0, 0.0), b: Vec3::new(20.0, 10.0, 0.0) }));
line.common.layer = "PARTS".into();
d.model.push(line);
let ins = Insert {
block: "NUT".into(),
insert: Vec3::new(10.0, 10.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
rotation: 0.0,
attribs: Vec::new(),
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
};
d.model.push(Entity::new(Handle(0x61), EntityKind::Insert(ins)));
Ok(d)
}
fn hooks() {
set_io(IoHooks { read, write: |_, _| Err("no writer in this test".into()), plot: None });
}
/// Lines in model space as (ax, ay, bx, by), rounded.
fn lines(s: &Session) -> Vec<(f64, f64, f64, f64)> {
let r = |v: f64| (v * 1e6).round() / 1e6;
s.doc()
.unwrap()
.model
.iter()
.filter_map(|e| match &e.kind {
EntityKind::Line(l) => Some((r(l.a.x), r(l.a.y), r(l.b.x), r(l.b.y))),
_ => None,
})
.collect()
}
/// Block references in model space as (block, x, y).
fn inserts(s: &Session) -> Vec<(String, f64, f64)> {
s.doc()
.unwrap()
.model
.iter()
.filter_map(|e| match &e.kind {
EntityKind::Insert(i) => Some((i.block.clone(), i.insert.x, i.insert.y)),
_ => None,
})
.collect()
}
#[test]
fn star_name_inserts_the_blocks_objects_with_one_scale_factor() {
let mut s = Session::new();
s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap();
s.execute("block", &json!({ "name": "B1", "base": [0, 0], "keep": "delete", "handles": ["100"] })).unwrap();
// Insertion point, then one scale factor and the rotation; no block reference is left.
s.script("-INSERT *B1 5,5 2 90\n").unwrap();
assert!(s.running.is_none(), "INSERT finished: {}", s.prompt_text());
assert!(inserts(&s).is_empty());
assert_eq!(lines(&s), vec![(5.0, 5.0, 5.0, 25.0)]);
// A plain name after an exploded one inserts a block reference again.
s.script("-INSERT B1 0,0 1 1 0\n").unwrap();
assert_eq!(inserts(&s).len(), 1);
// JSON: "*NAME" explodes too.
s.execute("insert", &json!({ "name": "*B1", "at": [0, 50] })).unwrap();
assert_eq!(inserts(&s).len(), 1);
assert_eq!(lines(&s).last(), Some(&(0.0, 50.0, 10.0, 50.0)));
}
#[test]
fn a_drawing_file_becomes_a_block_with_its_insbase_and_tables() {
hooks();
// Test drawing files hold their $INSBASE.
let dir = TempDir::new("insert-file");
let bolt = dir.file("bolt.dxf", "10,10");
let mut s = Session::new();
// This drawing's own layer PARTS keeps its colour.
s.execute("layer.new", &json!({ "name": "PARTS", "color": 3 })).unwrap();
s.script(&format!("-INSERT {bolt} 100,100 1 1 0\n")).unwrap();
assert!(s.running.is_none(), "INSERT finished: {}", s.prompt_text());
let d = s.doc().unwrap();
let b = d.block("bolt").expect("block named after the file");
assert_eq!(b.base, Vec3::new(10.0, 10.0, 0.0), "$INSBASE is the base point");
assert_eq!(b.entities.len(), 2);
assert!(d.block("NUT").is_some(), "nested blocks come along");
assert_eq!(d.layer("PARTS").map(|l| l.color), Some(Color::Index(3)), "an existing layer keeps its definition");
assert_eq!(inserts(&s), vec![("bolt".into(), 100.0, 100.0)]);
// The file again: its block exists, so INSERT asks; No (the default) keeps the definition.
s.cmdline(&format!("-INSERT {bolt}")).unwrap();
assert!(s.prompt_text().contains("Redefine"), "{}", s.prompt_text());
s.script("N 0,0 1 1 0\n").unwrap();
assert!(s.running.is_none(), "INSERT finished: {}", s.prompt_text());
assert_eq!(inserts(&s).len(), 2);
// NAME=path defines the block under that name without asking; *path inserts the file's objects.
let pin = dir.file("pin.dxf", "10,10");
s.script(&format!("-INSERT M8={bolt} 0,0 1 1 0\n-INSERT *{pin} 50,0 1 0\n")).unwrap();
assert!(s.running.is_none(), "INSERT finished: {}", s.prompt_text());
assert!(s.doc().unwrap().block("M8").is_some());
assert_eq!(inserts(&s).iter().filter(|i| i.0 == "M8").count(), 1);
// The exploded line, moved from the base point 10,10 to 50,0; the nested NUT stays a reference.
assert_eq!(lines(&s), vec![(50.0, 0.0, 60.0, 0.0)]);
assert!(inserts(&s).iter().any(|i| i.0 == "NUT" && i.1 == 50.0 && i.2 == 0.0));
// JSON: `file` with an optional block name, never a dialog.
let r = s.execute("insert", &json!({ "file": dir.file("washer.dxf", "0,0"), "at": [5, 5] })).unwrap();
assert!(r["handle"].is_string(), "{r}");
assert!(inserts(&s).iter().any(|i| i.0 == "washer"));
// A file that can't be read is reported; nothing is inserted.
let n = inserts(&s).len();
assert!(s.execute("insert", &json!({ "file": "/no/such/part.dxf", "at": [0, 0] })).is_err());
s.cmdline("-INSERT /no/such/part.dxf").unwrap();
assert!(s.log.iter().any(|m| m.contains("/no/such/part.dxf")), "{:?}", s.log.last());
assert!(s.prompt_text().contains("block name"), "{}", s.prompt_text());
assert_eq!(inserts(&s).len(), n);
}
#[test]
fn exploded_json_insert_is_one_undo_step() {
for params in [json!({ "name": "*B", "at": [5, 5] }), json!({ "name": "B", "at": [5, 5], "explode": true })] {
let mut s = Session::new();
s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap();
s.execute("block", &json!({ "name": "B", "base": [0, 0], "keep": "delete", "handles": ["100"] })).unwrap();
let undo = s.state().unwrap().undo.len();
s.execute("insert", &params).unwrap();
assert_eq!(lines(&s), vec![(5.0, 5.0, 15.0, 5.0)], "{params}");
assert_eq!(s.state().unwrap().undo.len(), undo + 1, "{params}");
s.execute("undo", &json!({})).unwrap();
assert!(lines(&s).is_empty(), "{params}");
assert!(inserts(&s).is_empty(), "{params}");
assert!(s.doc().unwrap().block("B").is_some(), "{params}");
// The second undo reverts the block creation, not a leftover EXPLODE step.
s.execute("undo", &json!({})).unwrap();
assert!(inserts(&s).is_empty(), "{params}");
assert!(s.doc().unwrap().block("B").is_none(), "{params}");
assert_eq!(lines(&s), vec![(0.0, 0.0, 10.0, 0.0)], "{params}");
}
}