From ba2531bc3f3e2d45d136969b5072f8c2e2c1b4c2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 09:33:36 -0800 Subject: [PATCH] Tests: engine tests remove the temp folders they make vectorcraft_testkit::temp_dir returns a bare path, so every engine test that wrote into one left the folder in the system temporary folder. - testkit: TempDir, a test's own folder made through temp_dir (same private creation and ownership checks) under a tag no other TempDir in the process uses, removed with its contents when dropped, also when the test panics. - Engine unit and integration tests hold a TempDir for the whole test instead of calling temp_dir: graph, fileio, compare, journal, library, shortcut set, spelling, store, web export, cancel, containers, build, color settings, command sweep, font folder, removed font, model-based and the font search fuzz. - The DCS and swatch library tests' own helpers and the PDF editing tests' shared vc-pdfedit- folder use a TempDir per test too. - The font and link file search, store, color settings and font folder helpers no longer delete the private folder temp_dir checked and make it again with default permissions. --- crates/engine/src/cmd/fileio/tests_pdfedit.rs | 46 +++++++----- .../src/cmd/fileio/tests_pdffidelity.rs | 3 +- .../engine/src/cmd/fileio/tests_readonly.rs | 3 +- crates/engine/src/cmd/graph/tests.rs | 35 +++++---- crates/engine/src/tests/build.rs | 2 +- crates/engine/src/tests/cancel.rs | 5 +- crates/engine/src/tests/compare.rs | 27 +++---- crates/engine/src/tests/containers.rs | 2 +- crates/engine/src/tests/dcs.rs | 11 +-- crates/engine/src/tests/fontfiles.rs | 39 +++++----- crates/engine/src/tests/journal.rs | 10 +-- crates/engine/src/tests/library.rs | 8 +-- crates/engine/src/tests/linkfiles.rs | 46 ++++++------ crates/engine/src/tests/shortcutset.rs | 6 +- crates/engine/src/tests/spelling.rs | 10 +-- crates/engine/src/tests/store.rs | 36 ++++++---- crates/engine/src/tests/swatchlib.rs | 8 +-- crates/engine/src/tests/webexport.rs | 4 +- crates/engine/tests/color_settings.rs | 13 ++-- crates/engine/tests/command_sweep.rs | 34 +++++---- crates/engine/tests/font_folder.rs | 8 +-- crates/engine/tests/import_fuzz.rs | 3 +- crates/engine/tests/it/model_based.rs | 5 +- crates/engine/tests/removed_font.rs | 5 +- crates/testkit/src/lib.rs | 71 +++++++++++++++++++ 25 files changed, 257 insertions(+), 183 deletions(-) diff --git a/crates/engine/src/cmd/fileio/tests_pdfedit.rs b/crates/engine/src/cmd/fileio/tests_pdfedit.rs index 0b560e5e04..b7b0ddde59 100644 --- a/crates/engine/src/cmd/fileio/tests_pdfedit.rs +++ b/crates/engine/src/cmd/fileio/tests_pdfedit.rs @@ -15,9 +15,8 @@ fn open(s: &mut Session, name: &str, bytes: &[u8]) -> Value { s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap() } -fn tmp(name: &str) -> String { - let dir = std::env::temp_dir().join(format!("vc-pdfedit-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); +/// The path of `name` in the test's folder `dir`. +fn tmp(dir: &std::path::Path, name: &str) -> String { dir.join(name).to_string_lossy().to_string() } @@ -114,8 +113,9 @@ fn the_legacy_payload_name_is_accepted() { #[test] fn ai_files_are_pdfs_that_save_and_reopen_editable() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = rich(); - let path = tmp("art.ai"); + let path = tmp(&scratch, "art.ai"); let r = s.execute("document.save", &json!({"path": path, "preserveEditing": false, "range": "1"})).unwrap(); assert_eq!(r["path"], path.as_str()); // As saved (Save stamps the File Info dates). @@ -134,11 +134,11 @@ fn ai_files_are_pdfs_that_save_and_reopen_editable() { let again = vectorcraft_pdf::editing(&std::fs::read(&path).unwrap()).unwrap(); assert_eq!(vectorcraft_format::load(&again.data).unwrap().layers, s.doc().unwrap().doc.layers); // Save As also writes a plain PDF (the document then saves back as PDF); PNG stays an export. - assert_eq!(s.execute("document.save", &json!({"path": tmp("x.pdf")})).unwrap()["format"], "pdf"); - assert!(s.execute("document.save", &json!({"path": tmp("x.png")})).is_err(), "PNG is written with document.export"); + assert_eq!(s.execute("document.save", &json!({"path": tmp(&scratch, "x.pdf")})).unwrap()["format"], "pdf"); + assert!(s.execute("document.save", &json!({"path": tmp(&scratch, "x.png")})).is_err(), "PNG is written with document.export"); // A plain PDF named .ai opens as artwork, without a path to save over. let plain = b64(&s.execute("document.exportPdf", &json!({"preserveEditing": false})).unwrap()); - let plain_path = tmp("plain.ai"); + let plain_path = tmp(&scratch, "plain.ai"); std::fs::write(&plain_path, plain).unwrap(); let r = s.execute("document.open", &json!({"path": plain_path})).unwrap(); assert_eq!(r["restored"], false); @@ -149,9 +149,10 @@ fn ai_files_are_pdfs_that_save_and_reopen_editable() { /// As does, leaving the document's own path alone. #[test] fn exports_write_ai_as_save_as_does() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = rich(); let before = comparable(&s.doc().unwrap().doc); - let path = tmp("exported.ai"); + let path = tmp(&scratch, "exported.ai"); let r = s.execute("document.export", &json!({"path": path})).unwrap(); assert_eq!(r["format"], "ai", "{r}"); let bytes = std::fs::read(&path).unwrap(); @@ -225,12 +226,13 @@ fn ait_templates_open_their_document_untitled() { /// from Template opens it as an untitled copy of the document, layers and art included. #[test] fn save_as_template_writes_an_ait_that_new_from_template_opens_untitled() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = rich(); s.execute("layer.new", &json!({"name": "Notes"})).unwrap(); s.execute("shape.ellipse", &json!({"x": 20, "y": 120, "width": 40, "height": 30})).unwrap(); let before = comparable(&s.doc().unwrap().doc); assert_eq!(before.layers.len(), 2); - let path = tmp("brochure template.ait"); + let path = tmp(&scratch, "brochure template.ait"); let r = s.execute("file.saveAsTemplate", &json!({"path": path})).unwrap(); assert_eq!(r["format"], "ait", "{r}"); let bytes = std::fs::read(&path).unwrap(); @@ -253,6 +255,7 @@ fn save_as_template_writes_an_ait_that_new_from_template_opens_untitled() { /// writes a `.ait`. #[test] fn save_as_template_names_its_file_after_the_template_format() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = rich(); for (name, format, saved) in [ ("card.png", None, "card.vctemplate"), @@ -261,7 +264,7 @@ fn save_as_template_names_its_file_after_the_template_format() { ("card.pdf", Some("ait"), "card.ait"), ("card v1.2", None, "card v1.2.vctemplate"), ] { - let (typed, want) = (tmp(&format!("named/{name}")), tmp(&format!("named/{saved}"))); + let (typed, want) = (tmp(&scratch, &format!("named/{name}")), tmp(&scratch, &format!("named/{saved}"))); let _ = std::fs::create_dir_all(std::path::Path::new(&typed).parent().unwrap()); let _ = std::fs::remove_file(&typed); let _ = std::fs::remove_file(&want); @@ -273,7 +276,7 @@ fn save_as_template_names_its_file_after_the_template_format() { assert!(ait || vectorcraft_format::load(&bytes).is_ok_and(|d| d.template), "{name}: a native template"); assert!(!std::path::Path::new(&typed).exists(), "{name}: nothing written under the typed name"); } - for (cmd, p) in [("file.saveAs", json!({"format": "ait"})), ("document.save", json!({"path": tmp("card.ait")}))] { + for (cmd, p) in [("file.saveAs", json!({"format": "ait"})), ("document.save", json!({"path": tmp(&scratch, "card.ait")}))] { let e = s.execute(cmd, &p).unwrap_err().to_string(); assert!(e.contains("file.saveAsTemplate") && !e.contains("document.export"), "{cmd}: {e}"); } @@ -284,8 +287,9 @@ fn save_as_template_names_its_file_after_the_template_format() { /// nothing writes keeps its own error. #[test] fn exports_of_an_ait_name_save_as_template() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = rich(); - let path = tmp("exported.ait"); + let path = tmp(&scratch, "exported.ait"); let _ = std::fs::remove_file(&path); for (cmd, p) in [ ("document.export", json!({"path": path})), @@ -305,8 +309,9 @@ fn exports_of_an_ait_name_save_as_template() { /// file; `aiVersion` "pdf" writes the PDF-compatible template. #[test] fn save_as_template_refuses_a_legacy_ait() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = rich(); - let path = tmp("legacy-ait/card.ait"); + let path = tmp(&scratch, "legacy-ait/card.ait"); let dir = std::path::Path::new(&path).parent().unwrap().to_path_buf(); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); @@ -424,7 +429,8 @@ fn an_ai_of_the_arts_bounds_carries_no_editing_data() { let mut s = art_outside_the_artboards(); let before = comparable(&s.doc().unwrap().doc); let left_out = |v: &Value| v["warnings"].as_array().unwrap().iter().any(|w| w == super::save::AI_EDITING_NEEDS_THE_ARTBOARDS); - let path = tmp("art-bounds.ai"); + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); + let path = tmp(&scratch, "art-bounds.ai"); for extra in [json!({}), json!({"range": "2"}), json!({"editingData": false}), json!({"path": path})] { let mut params = json!({"format": "ai", "useArtboards": false}); params.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone()); @@ -670,6 +676,7 @@ fn editing_copy(bytes: &[u8]) -> vectorcraft_doc::Document { /// writes the copy; the version 7 `.ai` keeps the compound path and the clipping group. #[test] fn transparency_clipping_and_opacity_masks_come_back_from_the_editing_copy() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = see_through_session(); let before = (*s.doc().unwrap().doc).clone(); // The mask's art is one path, and it reads back as that path. @@ -682,7 +689,7 @@ fn transparency_clipping_and_opacity_masks_come_back_from_the_editing_copy() { assert_eq!(see_through_outline(&editing_copy(&bytes)), want, "{opts}"); } // Each artboard in a file of its own: the first holds all the art. - let path = tmp("see-through.ai"); + let path = tmp(&scratch, "see-through.ai"); let r = s.execute("document.save", &json!({"path": path, "separateArtboards": true})).unwrap(); let files: Vec = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap().to_string()).collect(); assert_eq!(files.len(), 3, "{r}"); @@ -812,7 +819,8 @@ fn image_outline(doc: &vectorcraft_doc::Document) -> Vec { /// the linked image keeps its path and its path relative to the document's folder. #[test] fn images_come_back_from_the_editing_copy() { - let dir = tmp("ai-images"); + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); + let dir = tmp(&scratch, "ai-images"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(format!("{dir}/art")).unwrap(); let png = |w: u32, h: u32, f: &dyn Fn(u32, u32) -> [u8; 4]| { @@ -882,6 +890,7 @@ fn type_outline(doc: &vectorcraft_doc::Document) -> Vec { /// for other apps alone as type, with every save option that writes the copy. #[test] fn type_comes_back_from_the_editing_copy() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = Session::new(); s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap(); s.execute("text.create", &json!({"x": 20, "y": 40, "text": "Hello\nWorld", "size": 24})).unwrap(); @@ -889,7 +898,7 @@ fn type_comes_back_from_the_editing_copy() { // The new type is selected. s.execute("object.rotate", &json!({"angle": 30})).unwrap(); s.execute("text.create", &json!({"x": 20, "y": 120, "text": "Area type in a frame", "area": {"width": 120, "height": 60}})).unwrap(); - let path = tmp("type.ai"); + let path = tmp(&scratch, "type.ai"); s.execute("document.save", &json!({"path": path})).unwrap(); let before = type_outline(&s.doc().unwrap().doc); assert_eq!(before.len(), 3, "{before:#?}"); @@ -971,6 +980,7 @@ fn gradient_session() -> Session { /// gradient. #[test] fn gradients_and_patterns_come_back_from_the_editing_copy() { + let scratch = vectorcraft_testkit::TempDir::new("pdfedit"); let mut s = gradient_session(); let before = (*s.doc().unwrap().doc).clone(); let want = paint_outline(&before); @@ -990,7 +1000,7 @@ fn gradients_and_patterns_come_back_from_the_editing_copy() { let copy = editing_copy(&b64(&r)); assert_eq!(paint_outline(©), want, "{opts}"); } - let path = tmp("gradients.ai"); + let path = tmp(&scratch, "gradients.ai"); let r = s.execute("document.save", &json!({"path": path, "separateArtboards": true})).unwrap(); let files: Vec = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap().to_string()).collect(); assert_eq!(files.len(), 3, "{r}"); diff --git a/crates/engine/src/cmd/fileio/tests_pdffidelity.rs b/crates/engine/src/cmd/fileio/tests_pdffidelity.rs index 86cb84cfcb..d3d8a988f8 100644 --- a/crates/engine/src/cmd/fileio/tests_pdffidelity.rs +++ b/crates/engine/src/cmd/fileio/tests_pdffidelity.rs @@ -59,7 +59,7 @@ fn live_texts(s: &Session) -> Vec { #[test] fn pdf_place_keeps_live_text_from_bytes_and_unlinked_paths() { let bytes = live_text_pdf(); - let dir = vectorcraft_testkit::temp_dir("pdf-place-live-text"); + let dir = vectorcraft_testkit::TempDir::new("pdf-place-live-text"); let path = dir.join("text.pdf"); std::fs::write(&path, &bytes).unwrap(); for source in [json!({"name": "text.pdf", "dataBase64": vectorcraft_format::base64_encode(&bytes)}), json!({"path": path, "link": false})] { @@ -71,7 +71,6 @@ fn pdf_place_keeps_live_text_from_bytes_and_unlinked_paths() { assert_eq!(live_texts(&s), ["Second page"], "{r}"); assert!(!r["warnings"].to_string().contains("outlines"), "{r}"); } - std::fs::remove_dir_all(dir).unwrap(); } #[test] diff --git a/crates/engine/src/cmd/fileio/tests_readonly.rs b/crates/engine/src/cmd/fileio/tests_readonly.rs index badf82a8cf..ae9762efc3 100644 --- a/crates/engine/src/cmd/fileio/tests_readonly.rs +++ b/crates/engine/src/cmd/fileio/tests_readonly.rs @@ -9,7 +9,7 @@ use crate::{EngineError, READ_ONLY_LOCATION, Session}; #[test] fn readonly_error_is_typed_and_other_writers_keep_their_error() { - let dir = vectorcraft_testkit::temp_dir("save-refusal"); + let dir = vectorcraft_testkit::TempDir::new("save-refusal"); let path = dir.join("Poster.vectorcraft").to_string_lossy().into_owned(); for kind in [ErrorKind::PermissionDenied, ErrorKind::ReadOnlyFilesystem, ErrorKind::StorageFull, ErrorKind::NotFound, ErrorKind::Other] { let typed = write_save_file_with(&path, &mut |_| Err(io::Error::new(kind, "injected"))).unwrap_err(); @@ -26,7 +26,6 @@ fn readonly_error_is_typed_and_other_writers_keep_their_error() { } assert!(!std::path::Path::new(&path).exists()); } - std::fs::remove_dir_all(dir).unwrap(); } #[test] diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index fcc2a593c1..e8754c5dd2 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -1918,7 +1918,7 @@ fn series_colours_survive_native_save_open_and_export() { let second = group(s.doc().unwrap().doc.node(id).unwrap(), "2025").id; s.execute("paint.setFill", &json!({"ids": [first.0], "color": "#f00"})).unwrap(); s.execute("paint.setFill", &json!({"ids": [second.0], "color": "#008080"})).unwrap(); - let dir = vectorcraft_testkit::temp_dir("graph-palette"); + let dir = vectorcraft_testkit::TempDir::new("graph-palette"); let path = dir.join("palette.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); @@ -2332,7 +2332,8 @@ fn series_types_survive_save_and_open_and_older_files_have_none() { s.execute("file.new", &json!({})).unwrap(); let id = graph(&mut s, "column"); s.execute("graph.setType", &json!({"seriesIndexes": [1], "type": "line"})).unwrap(); - let path = vectorcraft_testkit::temp_dir("graph-kinds").join("kinds.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-kinds"); + let path = dir.join("kinds.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); assert_eq!(spec(&s, id).series_kinds, [None, Some(GraphKind::Line)]); @@ -2460,7 +2461,8 @@ fn value_axis_options_survive_save_and_open_and_are_left_out_when_unset() { let id = two_scale_graph(&mut s); s.execute("graph.setType", &json!({"valueAxis": "both", "separateScales": true, "rightAxisMin": 0, "rightAxisMax": 2000})).unwrap(); s.execute("graph.setType", &json!({"seriesIndexes": [1], "valueAxis": "right"})).unwrap(); - let path = vectorcraft_testkit::temp_dir("graph-axes").join("axes.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-axes"); + let path = dir.join("axes.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); let g = spec(&s, id); @@ -2662,7 +2664,8 @@ fn tick_and_label_options_survive_save_and_open_and_are_left_out_when_unset() { &json!({"tickLength": "full", "tickMarks": 3, "categoryTickLength": "short", "ticksBetweenLabels": true, "suffix": "%"}), ) .unwrap(); - let path = vectorcraft_testkit::temp_dir("graph-ticks").join("ticks.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-ticks"); + let path = dir.join("ticks.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); let g = spec(&s, id); @@ -2901,7 +2904,8 @@ fn designs_and_markers_survive_save_and_open() { save_design(&mut s, "Pill"); let id = graph(&mut s, "line"); s.execute("graph.marker", &json!({"seriesIndexes": [0], "design": "Pill"})).unwrap(); - let path = vectorcraft_testkit::temp_dir("graph-designs").join("designs.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-designs"); + let path = dir.join("designs.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); assert_eq!(s.execute("graph.design", &json!({})).unwrap()["designs"], json!(["Pill"])); @@ -3299,7 +3303,8 @@ fn column_designs_survive_save_and_open_and_follow_their_design() { assert!(series_parts(&s, id, 1).iter().all(|n| matches!(n.kind, NodeKind::Path { .. })), "the line series"); let v = s.execute("graph.column", &json!({"id": id.0, "seriesIndexes": [0]})).unwrap(); assert_eq!(v, json!({"designs": ["Coin"], "design": "Coin", "columnType": "repeating", "each": 2.5, "fractions": "scale"})); - let path = vectorcraft_testkit::temp_dir("graph-column-designs").join("columns.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-column-designs"); + let path = dir.join("columns.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); let options = ColumnDesign { design: "Coin".into(), column_type: ColumnType::Repeating, each: 2.5, fractions: Fractions::Scale }; @@ -3821,7 +3826,8 @@ fn legends_in_wedges_put_the_series_labels_inside_the_pie() { s.execute("graph.setType", &json!({"legend": true})).unwrap(); assert!(!spec(&s, id).pie_legend_in_wedges); assert!(s.execute("graph.setType", &json!({"pieLegend": "inside"})).is_err()); - let path = vectorcraft_testkit::temp_dir("graph-pie").join("pie.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-pie"); + let path = dir.join("pie.vectorcraft"); s.execute("graph.setType", &json!({"pieLegend": "wedges", "piePosition": "stacked", "pieSort": "all"})).unwrap(); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); @@ -3962,7 +3968,8 @@ fn the_legend_across_top_runs_in_rows_above_the_plot() { assert!(ys.iter().all(|y| *y + 8.0 <= 100.0 - 8.0 + 1e-6), "{ys:?}"); let xs: Vec = (0..6).map(|i| swatch(&s, i).x1).collect(); assert!(xs.iter().all(|x| *x <= 400.0), "{xs:?}"); - let path = vectorcraft_testkit::temp_dir("graph-top").join("top.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-top"); + let path = dir.join("top.vectorcraft"); s.execute("graph.setType", &json!({"dropShadow": true})).unwrap(); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); @@ -4081,7 +4088,8 @@ fn filled_lines_are_bands_in_the_series_colour() { s.execute("paint.setFill", &json!({"ids": [grp.0], "color": "#ff0000"})).unwrap(); s.execute("graph.setData", &json!({"id": id.0, "rows": [[2], [3], [2]]})).unwrap(); assert_eq!(series_parts(&s, id, 0)[0].appearance.fill_paint().color().unwrap().to_hex(), "#ff0000"); - let path = vectorcraft_testkit::temp_dir("graph-filled").join("filled.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-filled"); + let path = dir.join("filled.vectorcraft"); s.execute("graph.setType", &json!({"firstRowInFront": true, "firstColumnInFront": true})).unwrap(); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); @@ -4208,7 +4216,8 @@ fn a_graph_labels_anti_aliasing_method_survives_it_being_drawn_again() { s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(spec(&s, id).label_styles.categories_anti_alias, None); s.execute("edit.redo", &json!({})).unwrap(); - let path = vectorcraft_testkit::temp_dir("graph-label-anti-alias").join("labels.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-label-anti-alias"); + let path = dir.join("labels.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); assert_eq!(spec(&s, id).label_styles.categories_anti_alias, Some(TypeAntiAlias::Strong)); @@ -4251,7 +4260,8 @@ fn formatting_one_label_keeps_that_kind_of_label() { assert_eq!(look("0").2, "#0000ff", "every value axis number"); assert_eq!(look("0").1, 9.0); // Kept through save and open; nothing written when unset. - let path = vectorcraft_testkit::temp_dir("graph-labels").join("labels.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-labels"); + let path = dir.join("labels.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); assert_eq!(spec(&s, id).label_styles.categories.as_ref().unwrap().size, 14.0); @@ -4420,7 +4430,8 @@ fn paragraph_formats_are_kept_when_the_graph_is_drawn_again() { let v = text_id(&s, col, "Axes", "4"); s.execute("select.set", &json!({"ids": [v.0]})).unwrap(); s.execute("text.setFormat", &json!({"composer": "singleLine"})).unwrap(); - let path = vectorcraft_testkit::temp_dir("graph-label-paras").join("labels.vectorcraft"); + let dir = vectorcraft_testkit::TempDir::new("graph-label-paras"); + let path = dir.join("labels.vectorcraft"); s.execute("document.save", &json!({"path": path})).unwrap(); s.execute("document.open", &json!({"path": path})).unwrap(); s.execute("select.set", &json!({"ids": [col.0]})).unwrap(); diff --git a/crates/engine/src/tests/build.rs b/crates/engine/src/tests/build.rs index 21cbf678eb..6eeace1073 100644 --- a/crates/engine/src/tests/build.rs +++ b/crates/engine/src/tests/build.rs @@ -840,7 +840,7 @@ fn shape_builder_applies_a_merge_before_what_comes_next() { }), ]; let _proof = crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - let dir = vectorcraft_testkit::temp_dir("shape-builder-merge-first"); + let dir = vectorcraft_testkit::TempDir::new("shape-builder-merge-first"); for (what, then) in next { let mut s = session(); if what == "Select All Instances" { diff --git a/crates/engine/src/tests/cancel.rs b/crates/engine/src/tests/cancel.rs index 8237707a10..9a044ead89 100644 --- a/crates/engine/src/tests/cancel.rs +++ b/crates/engine/src/tests/cancel.rs @@ -22,7 +22,8 @@ fn revision(s: &Session) -> u64 { #[test] fn a_pdf_export_cancelled_between_pages_stops_writes_nothing_and_changes_nothing() { - let path = vectorcraft_testkit::temp_dir("cancel-pdf").join("out.pdf"); + let dir = vectorcraft_testkit::TempDir::new("cancel-pdf"); + let path = dir.join("out.pdf"); std::fs::remove_file(&path).ok(); let mut s = session(5); let before = (revision(&s), s.doc().unwrap().history.undo.len()); @@ -66,7 +67,7 @@ fn a_cancelled_flattening_export_stops_before_the_pages() { #[test] fn raster_exports_and_svg_stop_when_cancelled_too() { - let dir = vectorcraft_testkit::temp_dir("cancel-formats"); + let dir = vectorcraft_testkit::TempDir::new("cancel-formats"); let mut s = session(3); let token = CancelToken::new(); token.cancel(); diff --git a/crates/engine/src/tests/compare.rs b/crates/engine/src/tests/compare.rs index 3bd4446d61..3a6c66e2f5 100644 --- a/crates/engine/src/tests/compare.rs +++ b/crates/engine/src/tests/compare.rs @@ -36,7 +36,7 @@ fn error(s: &mut Session, p: Value) -> String { #[test] fn an_export_compares_equal_and_a_moved_square_counts_the_pixels_it_changed() { - let dir = vectorcraft_testkit::temp_dir("compare-moved"); + let dir = vectorcraft_testkit::TempDir::new("compare-moved"); let png = path_in(&dir, "art.png"); let (mut s, id) = session(); export(&mut s, &png); @@ -54,12 +54,11 @@ fn an_export_compares_equal_and_a_moved_square_counts_the_pixels_it_changed() { assert_eq!(r["rmse"].as_f64(), Some(rmse), "{r}"); // Above every difference, nothing is over. assert_eq!(compare(&mut s, json!({"path": png, "threshold": 1}))["over"], json!(0)); - let _ = std::fs::remove_dir_all(dir); } #[test] fn the_image_and_the_render_are_flattened_on_the_background() { - let dir = vectorcraft_testkit::temp_dir("compare-background"); + let dir = vectorcraft_testkit::TempDir::new("compare-background"); let png = path_in(&dir, "clear.png"); // Transparent everywhere, with red in the color channels that the alpha hides. image::RgbaImage::from_pixel(80, 40, image::Rgba([255, 0, 0, 0])).save(&png).unwrap(); @@ -81,12 +80,11 @@ fn the_image_and_the_render_are_flattened_on_the_background() { assert_eq!((r["rmse"].as_f64(), r["over"].as_u64()), (Some(rmse(255.0)), Some(1600)), "{r}"); // A threshold just above 128/255 leaves the gray differences out. assert_eq!(compare(&mut s, json!({"path": png, "background": "#808080", "threshold": 0.51}))["over"], json!(0)); - let _ = std::fs::remove_dir_all(dir); } #[test] fn an_image_of_other_proportions_is_refused_and_one_pixel_is_allowed() { - let dir = vectorcraft_testkit::temp_dir("compare-size"); + let dir = vectorcraft_testkit::TempDir::new("compare-size"); let (mut s, _) = session(); let square = path_in(&dir, "square.png"); image::RgbaImage::from_pixel(30, 30, image::Rgba([255; 4])).save(&square).unwrap(); @@ -96,12 +94,11 @@ fn an_image_of_other_proportions_is_refused_and_one_pixel_is_allowed() { image::RgbaImage::from_pixel(80, 41, image::Rgba([255; 4])).save(&taller).unwrap(); let r = compare(&mut s, json!({"path": taller})); assert_eq!((r["width"].as_u64(), r["height"].as_u64(), r["total"].as_u64()), (Some(80), Some(41), Some(3280)), "{r}"); - let _ = std::fs::remove_dir_all(dir); } #[test] fn an_export_whose_height_was_rounded_compares_equal() { - let dir = vectorcraft_testkit::temp_dir("compare-rounded"); + let dir = vectorcraft_testkit::TempDir::new("compare-rounded"); let png = path_in(&dir, "art.png"); // 20.25 pt at 2 pixels a point is 40.5 rows, which the export rounds to 41. let mut s = Session::new(); @@ -112,7 +109,6 @@ fn an_export_whose_height_was_rounded_compares_equal() { export(&mut s, &png); let r = compare(&mut s, json!({"path": png})); assert_eq!(r, json!({"width": 80, "height": 41, "rmse": 0.0, "over": 0, "total": 3280, "threshold": 0.08})); - let _ = std::fs::remove_dir_all(dir); } #[test] @@ -151,7 +147,7 @@ fn a_linked_image_is_drawn_from_its_file_as_a_png_export_draws_it() { #[test] fn an_image_that_cannot_be_decoded_and_a_missing_font_warn_as_a_png_export_does() { - let dir = vectorcraft_testkit::temp_dir("compare-warnings"); + let dir = vectorcraft_testkit::TempDir::new("compare-warnings"); let png = path_in(&dir, "art.png"); let (mut s, _) = session(); export(&mut s, &png); @@ -167,12 +163,11 @@ fn an_image_that_cannot_be_decoded_and_a_missing_font_warn_as_a_png_export_does( assert!(warnings.iter().any(|w| w == "an image couldn't be decoded and is missing from the picture"), "{r}"); assert!(warnings.iter().any(|w| w.as_str().unwrap().contains("Nonexistent Sans Probe")), "{r}"); assert!(warnings.iter().any(|w| w.as_str().unwrap().contains("doesn't fit its frame")), "{r}"); - let _ = std::fs::remove_dir_all(dir); } #[test] fn diff_path_writes_each_pixels_differences() { - let dir = vectorcraft_testkit::temp_dir("compare-diff"); + let dir = vectorcraft_testkit::TempDir::new("compare-diff"); let png = path_in(&dir, "art.png"); let diff = path_in(&dir, "diff.png"); let (mut s, id) = session(); @@ -188,12 +183,11 @@ fn diff_path_writes_each_pixels_differences() { } let changed = img.pixels().filter(|p| p.0[0] != 0).count(); assert_eq!(changed as u64, r["over"].as_u64().unwrap()); - let _ = std::fs::remove_dir_all(dir); } #[test] fn missing_unreadable_and_malformed_inputs_are_errors() { - let dir = vectorcraft_testkit::temp_dir("compare-errors"); + let dir = vectorcraft_testkit::TempDir::new("compare-errors"); let (mut s, _) = session(); let png = path_in(&dir, "art.png"); export(&mut s, &png); @@ -216,14 +210,13 @@ fn missing_unreadable_and_malformed_inputs_are_errors() { let e = error(&mut s, p.clone()); assert!(e.contains(want), "{p}: {e}"); } - let _ = std::fs::remove_dir_all(dir); } /// A planar TIFF whose header claims more strips than the decoder can count (2^31 + 1 rows a strip /// each, times 5 planes) is refused as too large to draw, without a panic. #[test] fn a_tiff_with_more_strips_than_can_be_counted_is_too_large() { - let dir = vectorcraft_testkit::temp_dir("compare-strips"); + let dir = vectorcraft_testkit::TempDir::new("compare-strips"); let (mut s, _) = session(); let plane = |v: u16| vec![v; 6]; let channels = [plane(200), plane(40), plane(10), plane(0), plane(90)]; @@ -247,12 +240,11 @@ fn a_tiff_with_more_strips_than_can_be_counted_is_too_large() { std::fs::write(&tif, &file).unwrap(); let e = error(&mut s, json!({"path": tif})); assert_eq!(e, "invalid parameters for `document.compare`: an image of 3 × 2147483649 pixels is too large to draw"); - let _ = std::fs::remove_dir_all(dir); } #[test] fn the_image_and_the_difference_image_stay_inside_the_automation_roots() { - let dir = vectorcraft_testkit::temp_dir("compare-roots"); + let dir = vectorcraft_testkit::TempDir::new("compare-roots"); for d in ["in", "out", "elsewhere"] { std::fs::create_dir_all(dir.join(d)).unwrap(); } @@ -272,5 +264,4 @@ fn the_image_and_the_difference_image_stay_inside_the_automation_roots() { assert!(!std::path::Path::new(&stray).exists()); let kept = path_in(&dir.join("out"), "diff.png"); assert_eq!(run(json!({"path": inside, "diffPath": kept})).unwrap()["diffPath"], json!(kept)); - let _ = std::fs::remove_dir_all(dir); } diff --git a/crates/engine/src/tests/containers.rs b/crates/engine/src/tests/containers.rs index 437f932724..42cebea2ad 100644 --- a/crates/engine/src/tests/containers.rs +++ b/crates/engine/src/tests/containers.rs @@ -312,7 +312,7 @@ fn an_images_stroke_with_outline_object_follows_its_opaque_area() { run(&mut s, "effect.apply", json!({"effect": "path.offsetPath", "params": {"offset": 3}, "item": 0})); assert_eq!([shade(&s, 61, 100), shade(&s, 64, 100)], ["white", "black"]); // Relinked to an opaque image, the stroke follows the frame 3 pt out: it shows from 43 to 50. - let dir = vectorcraft_testkit::temp_dir("outline-object-relink"); + let dir = vectorcraft_testkit::TempDir::new("outline-object-relink"); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("opaque.png"); let mut png = vec![]; diff --git a/crates/engine/src/tests/dcs.rs b/crates/engine/src/tests/dcs.rs index 5c806884d8..b780f14c09 100644 --- a/crates/engine/src/tests/dcs.rs +++ b/crates/engine/src/tests/dcs.rs @@ -3,8 +3,6 @@ //! the Links panel's Show DCS Transparency Interactions (`links.list` with `dcsTransparency`). //! The files are made from scratch as http://www.telegraphics.net/doc/DCS.txt describes them. -use std::path::PathBuf; - use serde_json::{Value, json}; use vectorcraft_color::{Color, Paint}; use vectorcraft_doc::{Node, NodeKind}; @@ -17,12 +15,9 @@ fn session() -> Session { s } -/// A fresh scratch folder for one test. -fn temp_dir(name: &str) -> PathBuf { - let d = std::env::temp_dir().join(format!("vc-dcs-{name}-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&d); - std::fs::create_dir_all(&d).unwrap(); - d +/// A fresh scratch folder for one test, removed when dropped. +fn temp_dir(name: &str) -> vectorcraft_testkit::TempDir { + vectorcraft_testkit::TempDir::new(&format!("dcs-{name}")) } /// A plate program: one 2 × 2 grayscale image (`gray`, row by row) on a 20 × 10 pt page. diff --git a/crates/engine/src/tests/fontfiles.rs b/crates/engine/src/tests/fontfiles.rs index 8bef8733b5..52a8ff08a3 100644 --- a/crates/engine/src/tests/fontfiles.rs +++ b/crates/engine/src/tests/fontfiles.rs @@ -14,12 +14,12 @@ use crate::cmd::findfiles::{self, Limits, Rules, Visitor}; use crate::cmd::fontfiles::{self, FontSearch, MAX_FONT_FILE, Sought, copy_into}; use crate::{EngineError, Session}; -/// A fresh temporary folder (canonical, as a search reports it). -fn folder(tag: &str) -> PathBuf { - let dir = vectorcraft_testkit::temp_dir(&format!("fontfiles-{tag}")); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - findfiles::plain(std::fs::canonicalize(&dir).unwrap()) +/// A fresh temporary folder, removed when the first part is dropped, and its path (canonical, as a +/// search reports it). +fn folder(tag: &str) -> (vectorcraft_testkit::TempDir, PathBuf) { + let dir = vectorcraft_testkit::TempDir::new(&format!("fontfiles-{tag}")); + let path = findfiles::plain(std::fs::canonicalize(&*dir).unwrap()); + (dir, path) } /// A session whose folder searches may go anywhere in `root`, on two threads. @@ -103,7 +103,7 @@ fn missing_fonts_are_listed_symbols_included() { /// listed finds the other fonts' files. #[test] fn an_overlong_font_name_leaves_the_other_fonts_searchable() { - let root = folder("long-name"); + let (_dir, root) = folder("long-name"); std::fs::write(root.join("Findme.otf"), vectorcraft_testkit::fonts::renamed("Findme Sans 3")).unwrap(); let mut s = session_for(&root); s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap(); @@ -120,7 +120,7 @@ fn an_overlong_font_name_leaves_the_other_fonts_searchable() { /// apps' folders, such as the attachments a mail app keeps in the home folder's Library. #[test] fn the_fonts_folder_next_to_a_document_in_another_apps_folder_isnt_offered() { - let root = folder("next-to"); + let (_dir, root) = folder("next-to"); let home = root.join("Users/me"); let mut s = Session::new(); s.search_rules = Some(Rules { home: Some(home.clone()), ..Rules::default() }); @@ -140,7 +140,7 @@ fn the_fonts_folder_next_to_a_document_in_another_apps_folder_isnt_offered() { #[cfg(unix)] #[test] fn the_fonts_folder_next_to_a_document_is_an_absolute_path() { - let root = folder("relative"); + let (_dir, root) = folder("relative"); std::fs::create_dir_all(root.join("pkg/Fonts")).unwrap(); let cwd = std::env::current_dir().unwrap(); let up = "../".repeat(cwd.components().count().saturating_sub(1)); @@ -160,7 +160,7 @@ fn the_fonts_folder_next_to_a_document_is_an_absolute_path() { #[test] fn a_search_finds_a_missing_fonts_file_by_its_names() { - let root = folder("find"); + let (_dir, root) = folder("find"); std::fs::create_dir_all(root.join("x/y")).unwrap(); std::fs::write(root.join("x/y/Findme.otf"), vectorcraft_testkit::fonts::renamed("Findme Sans 3")).unwrap(); std::fs::write(root.join("other.ttf"), bundled("SourceSerif4-Regular.ttf")).unwrap(); @@ -199,7 +199,7 @@ fn a_search_finds_a_missing_fonts_file_by_its_names() { /// when they are inside them. #[test] fn a_search_shows_a_confined_caller_only_paths_inside_its_read_roots() { - let root = folder("read-roots"); + let (_dir, root) = folder("read-roots"); let font = vectorcraft_testkit::fonts::renamed("Findme Sans 3"); let (home, wide) = (root.join("home"), root.join("lrf-wide")); for dir in [&home, &wide] { @@ -228,12 +228,11 @@ fn a_search_shows_a_confined_caller_only_paths_inside_its_read_roots() { assert!(finished(&mut s)["error"].as_str().is_some_and(|e| e.contains("lrf-wide"))); let r = ask(&mut s, &narrow, json!({})); assert_eq!((r["state"].as_str(), r["error"].as_str(), r.get("folder")), (Some("failed"), Some("the folder can't be read"), None), "{r}"); - let _ = std::fs::remove_dir_all(&root); } #[test] fn junk_search_params_never_fail_internally() { - let root = folder("junk"); + let (_dir, root) = folder("junk"); let mut s = session_for(&root); s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap(); text(&mut s, 20.0, "No Such Font Family", "Regular"); @@ -297,7 +296,7 @@ impl Visitor for Blocked { #[test] fn stop_ends_only_the_search_it_names() { - let root = folder("stop"); + let (_dir, root) = folder("stop"); std::fs::write(root.join("a.ttf"), b"font").unwrap(); let mut s = session_for(&root); let (tx, rx) = mpsc::channel(); @@ -330,7 +329,7 @@ fn adding_fonts_needs_vectorcraft_s_fonts_folder() { #[test] fn copying_never_replaces_a_file() { - let root = folder("copy"); + let (_dir, root) = folder("copy"); let (src, dest) = (root.join("src"), root.join("dest")); std::fs::create_dir_all(&src).unwrap(); std::fs::create_dir_all(&dest).unwrap(); @@ -376,12 +375,11 @@ fn copying_never_replaces_a_file() { assert_eq!(reasons[..2], ["larger than 256 MB", "not a file"]); assert!(r.copied.is_empty() && reasons.len() == 3, "{r:?}"); assert!(!dest.join("huge.otf").exists() && !dest.join("folder.otf").exists()); - let _ = std::fs::remove_dir_all(&root); } #[test] fn only_font_files_are_copied() { - let root = folder("allowed"); + let (_dir, root) = folder("allowed"); let (src, dest) = (root.join("src"), root.join("dest")); std::fs::create_dir_all(&src).unwrap(); std::fs::create_dir_all(&dest).unwrap(); @@ -395,13 +393,12 @@ fn only_font_files_are_copied() { let not_a_font = |n: &str| (src.join(n), "not a font file".to_string()); assert_eq!(r.skipped, [not_a_font("notes.txt"), not_a_font("Script")]); assert!(!dest.join("notes.txt").exists() && !dest.join("Script").exists()); - let _ = std::fs::remove_dir_all(&root); } /// A search reads no font file larger than Add Fonts copies, and finds a smaller copy of the font. #[test] fn a_search_skips_font_files_too_large_to_add() { - let root = folder("too-large"); + let (_dir, root) = folder("too-large"); let (big, small) = (root.join("big").join("Sizeme.otf"), root.join("small").join("Sizeme.otf")); let font = vectorcraft_testkit::fonts::renamed("Sizeme Sans 3"); for path in [&big, &small] { @@ -415,7 +412,6 @@ fn a_search_skips_font_files_too_large_to_add() { s.execute("text.findFontFiles", &json!({"folder": root, "fonts": [{"family": "Sizeme Sans 3"}, {"family": "No Such Sans"}]})).unwrap(); let r = finished(&mut s); assert_eq!(r["fonts"][0]["files"], json!([small]), "{r}"); - let _ = std::fs::remove_dir_all(&root); } /// A suitcase font: a file without an extension or with `.suit`, empty but for the fonts in its @@ -424,7 +420,7 @@ fn a_search_skips_font_files_too_large_to_add() { #[test] fn suitcase_fonts_are_found_and_copied_with_their_fork() { use vectorcraft_testkit::fonts::{renamed, suitcase_fork}; - let root = folder("suitcase"); + let (_dir, root) = folder("suitcase"); let (src, dest) = (root.join("src"), root.join("dest")); std::fs::create_dir_all(src.join("sub")).unwrap(); std::fs::create_dir_all(&dest).unwrap(); @@ -467,5 +463,4 @@ fn suitcase_fonts_are_found_and_copied_with_their_fork() { // The font scan reads the copies. let db = vectorcraft_text::FontDb::with_font_dirs(vec![dest.clone()]); assert_eq!((db.styles("Suitme Sans 3"), db.styles("Other Sans 33")), (vec!["Regular".to_string()], vec!["Regular".to_string()])); - let _ = std::fs::remove_dir_all(&root); } diff --git a/crates/engine/src/tests/journal.rs b/crates/engine/src/tests/journal.rs index 5eb66ec6c8..159f0604e5 100644 --- a/crates/engine/src/tests/journal.rs +++ b/crates/engine/src/tests/journal.rs @@ -275,7 +275,8 @@ fn a_batch_fit_selection_replays_but_an_action_uses_the_current_selection() { #[test] fn a_save_records_its_date_and_replays_it() { - let path = vectorcraft_testkit::temp_dir("journal-save").join("a.vectorcraft").to_string_lossy().into_owned(); + let dir = vectorcraft_testkit::TempDir::new("journal-save"); + let path = dir.join("a.vectorcraft").to_string_lossy().into_owned(); let mut s = Session::new(); s.execute("file.new", &json!({"created": null})).unwrap(); s.execute("document.save", &json!({"path": path})).unwrap(); @@ -290,7 +291,6 @@ fn a_save_records_its_date_and_replays_it() { assert_eq!(s.doc().unwrap().doc.metadata.modified, Some(2_000_000_000)); assert!(s.execute("document.save", &json!({"path": path, "modified": "today"})).is_err()); // Save As stamps the same way; Save a Copy leaves the document's dates (and records none). - let dir = vectorcraft_testkit::temp_dir("journal-save"); s.execute("file.saveAs", &json!({"path": dir.join("b.vectorcraft").to_string_lossy(), "modified": 2_000_000_100})).unwrap(); assert_eq!(s.journal.last().unwrap().1["modified"], json!(2_000_000_100)); s.execute("file.saveCopy", &json!({"path": dir.join("c.vectorcraft").to_string_lossy()})).unwrap(); @@ -316,7 +316,8 @@ fn replayed(s: &Session) -> Session { #[test] fn a_save_in_a_batch_records_its_date_and_replays_it() { - let path = vectorcraft_testkit::temp_dir("journal-batch").join("a.vectorcraft").to_string_lossy().into_owned(); + let dir = vectorcraft_testkit::TempDir::new("journal-batch"); + let path = dir.join("a.vectorcraft").to_string_lossy().into_owned(); let mut s = Session::new(); s.execute("file.new", &json!({"created": 1_000_000_000})).unwrap(); let rect = json!({"x": 10, "y": 10, "width": 50, "height": 40}); @@ -698,7 +699,8 @@ fn a_failed_batch_closes_the_documents_it_opened_and_restores_the_ones_it_closed #[test] fn a_failed_batch_brings_back_a_document_it_reverted() { - let path = vectorcraft_testkit::temp_dir("journal-batch").join("revert.vectorcraft").to_string_lossy().into_owned(); + let dir = vectorcraft_testkit::TempDir::new("journal-batch"); + let path = dir.join("revert.vectorcraft").to_string_lossy().into_owned(); let mut s = Session::new(); s.execute("file.new", &json!({"created": null})).unwrap(); s.execute("document.save", &json!({"path": path, "modified": null})).unwrap(); diff --git a/crates/engine/src/tests/library.rs b/crates/engine/src/tests/library.rs index d7927ec6bf..c2dccc7bb5 100644 --- a/crates/engine/src/tests/library.rs +++ b/crates/engine/src/tests/library.rs @@ -816,7 +816,7 @@ fn style_order_of(s: &mut Session) -> Vec { #[cfg(not(target_arch = "wasm32"))] #[test] fn a_library_item_rename_is_saved_or_taken_back() { - let dir = vectorcraft_testkit::temp_dir("library-rename-item"); + let dir = vectorcraft_testkit::TempDir::new("library-rename-item"); let folder = dir.to_string_lossy().to_string(); let mut s = session(); s.libraries.set_dir(Some(folder.clone())); @@ -836,7 +836,6 @@ fn a_library_item_rename_is_saved_or_taken_back() { run(&mut s, "library.renameItem", json!({"kind": "charStyle", "item": "Text", "name": " Text "})); assert!(s.execute("library.renameItem", &json!({"kind": "charStyle", "item": "Text", "name": "Copy"})).is_err()); assert_eq!(style_order_of(&mut s), ["Text"], "taken back"); - let _ = std::fs::remove_dir_all(&dir); } #[cfg(not(target_arch = "wasm32"))] @@ -1056,7 +1055,8 @@ fn a_new_item_is_added_where_it_is_dropped() { #[cfg(not(target_arch = "wasm32"))] #[test] fn an_add_that_cannot_be_placed_makes_no_library() { - let folder = vectorcraft_testkit::temp_dir("library-add-unplaced").to_string_lossy().to_string(); + let dir = vectorcraft_testkit::TempDir::new("library-add-unplaced"); + let folder = dir.to_string_lossy().to_string(); let mut s = session(); s.libraries.set_dir(Some(folder.clone())); for bad in [json!({"kind": "fillColor", "color": "#808080", "group": "Nope"}), json!({"kind": "fillColor", "color": "#808080", "after": "Nope"})] @@ -1163,7 +1163,7 @@ fn a_library_of_one_repeated_name_imports_numbered() { #[cfg(not(target_arch = "wasm32"))] #[test] fn a_library_is_exported_to_a_file_and_imported_from_it() { - let dir = vectorcraft_testkit::temp_dir("library-export"); + let dir = vectorcraft_testkit::TempDir::new("library-export"); let path = dir.join("Brand.vclibrary").to_string_lossy().to_string(); let (mut s, g, ..) = brand(); run(&mut s, "library.createGroup", json!({"name": "Logos", "items": [{"kind": "graphic", "item": g}]})); diff --git a/crates/engine/src/tests/linkfiles.rs b/crates/engine/src/tests/linkfiles.rs index ef9127042a..6f64a2fe33 100644 --- a/crates/engine/src/tests/linkfiles.rs +++ b/crates/engine/src/tests/linkfiles.rs @@ -14,12 +14,12 @@ use crate::cmd::linkfiles::{self, LinkSearch, Sought}; use crate::tests_links::{BLUE, RED, place, png}; use crate::{EngineError, Session}; -/// A fresh temporary folder (canonical, as a search reports it). -fn folder(tag: &str) -> PathBuf { - let dir = vectorcraft_testkit::temp_dir(&format!("linkfiles-{tag}")); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - findfiles::plain(std::fs::canonicalize(&dir).unwrap()) +/// A fresh temporary folder, removed when the first part is dropped, and its path (canonical, as a +/// search reports it). +fn folder(tag: &str) -> (vectorcraft_testkit::TempDir, PathBuf) { + let dir = vectorcraft_testkit::TempDir::new(&format!("linkfiles-{tag}")); + let path = findfiles::plain(std::fs::canonicalize(&*dir).unwrap()); + (dir, path) } /// Write `bytes` to the file at `path`, making its folders. @@ -80,7 +80,7 @@ fn found(r: &Value, name: &str, root: &Path) -> Vec<(String, String, bool)> { /// innermost out up to the first that differs. #[test] fn files_found_are_ranked_by_what_the_link_saved() { - let root = folder("rank"); + let (_dir, root) = folder("rank"); let mut s = session_for(&root); let (logo, photo) = (png(60, 40, RED), png(50, 30, BLUE)); write(&root.join("orig/logo.png"), &logo); @@ -127,7 +127,7 @@ fn files_found_are_ranked_by_what_the_link_saved() { /// the files found are ranked by the folder names they share with that path. #[test] fn a_classic_mac_path_is_found_by_its_name_and_folders() { - let root = folder("classic"); + let (_dir, root) = folder("classic"); let mut s = session_for(&root); let photo = png(50, 30, BLUE); write(&root.join("orig/photo.png"), &photo); @@ -158,7 +158,7 @@ fn a_classic_mac_path_is_found_by_its_name_and_folders() { #[test] fn parent_components_in_a_saved_path_are_resolved_before_ranking() { for (tag, saved) in [("classic-up", "Old Disk:Projects:Art::photo.png"), ("slash-up", "/old/Projects/Art/../photo.png")] { - let root = folder(tag); + let (_dir, root) = folder(tag); let mut s = session_for(&root); let photo = png(50, 30, BLUE); write(&root.join("orig/photo.png"), &photo); @@ -188,7 +188,7 @@ fn parent_components_in_a_saved_path_are_resolved_before_ranking() { /// files, whatever their status. #[test] fn a_search_looks_for_the_missing_files_or_the_objects_given() { - let root = folder("which"); + let (_dir, root) = folder("which"); let mut s = session_for(&root); write(&root.join("orig/gone.png"), &png(40, 30, RED)); write(&root.join("orig/here.png"), &png(40, 30, BLUE)); @@ -225,7 +225,7 @@ fn a_search_looks_for_the_missing_files_or_the_objects_given() { /// other contents leaves it to search every folder. #[test] fn a_search_ends_once_every_linked_file_has_its_same_file() { - let root = folder("early"); + let (_dir, root) = folder("early"); let mut s = session_for(&root); s.search_threads = Some(1); let bytes = png(40, 30, RED); @@ -265,7 +265,7 @@ impl Visitor for Blocked { #[test] fn stop_ends_only_the_search_it_names() { - let root = folder("stop"); + let (_dir, root) = folder("stop"); std::fs::write(root.join("a.png"), b"png").unwrap(); let mut s = session_for(&root); let (tx, rx) = mpsc::channel(); @@ -290,7 +290,7 @@ fn stop_ends_only_the_search_it_names() { /// roots, the replies are as before. #[test] fn a_search_shows_a_confined_caller_only_paths_inside_its_read_roots() { - let root = folder("read-roots"); + let (_dir, root) = folder("read-roots"); let mut s = session_for(&root); let photo = png(40, 30, RED); write(&root.join("lrf-away/photo.png"), &photo); @@ -343,14 +343,13 @@ fn a_search_shows_a_confined_caller_only_paths_inside_its_read_roots() { let r = ask(&mut s, &narrow, json!({})); assert_eq!((r["state"].as_str(), r["error"].as_str()), (Some("failed"), Some("the folder can't be read")), "{r}"); hidden(&r); - let _ = std::fs::remove_dir_all(&root); } /// A link's saved path relative to the document is shown to a confined caller when it leads to a /// file inside its read roots, and left out when `..` leads out of them. #[test] fn a_relative_link_path_is_judged_from_the_documents_folder() { - let root = folder("read-roots-relative"); + let (_dir, root) = folder("read-roots-relative"); let mut s = session_for(&root); let home = root.join("home"); write(&root.join("lrf-away/photo.png"), &png(40, 30, RED)); @@ -370,14 +369,13 @@ fn a_relative_link_path_is_judged_from_the_documents_folder() { assert_eq!(r["links"][0].get("path"), shown.then(|| json!(saved)).as_ref(), "{saved}: {r}"); finished(&mut s); } - let _ = std::fs::remove_dir_all(&root); } /// `image.info` leaves a linked file outside the confined caller's read roots unnamed (`link` /// null), as `links.info` leaves it out, and keeps the other fields. #[test] fn image_info_leaves_out_a_link_outside_the_read_roots() { - let root = folder("read-roots-info"); + let (_dir, root) = folder("read-roots-info"); let mut s = session_for(&root); let pic = root.join("lrf-away/photo.png"); write(&pic, &png(40, 30, RED)); @@ -391,12 +389,11 @@ fn image_info_leaves_out_a_link_outside_the_read_roots() { let info = ask(&mut s, "links.info"); assert!(info.get("link").is_none() && !info.to_string().contains("lrf-away"), "{info}"); assert_eq!(s.execute("image.info", &json!({"id": id})).unwrap()["link"], json!(pic)); - let _ = std::fs::remove_dir_all(&root); } #[test] fn relinking_with_copy_puts_the_file_in_the_documents_folder() { - let root = folder("copy"); + let (_dir, root) = folder("copy"); let mut s = session_for(&root); write(&root.join("orig/photo.png"), &png(40, 30, RED)); let id = placed(&mut s, &root.join("orig/photo.png")); @@ -443,14 +440,13 @@ fn relinking_with_copy_puts_the_file_in_the_documents_folder() { let image = placed(&mut unsaved, &found); let r = unsaved.execute("links.relink", &json!({"ids": [image], "path": found, "copy": true})); assert!(matches!(&r, Err(EngineError::BadParams { msg, .. }) if msg.contains("save the document first")), "{r:?}"); - let _ = std::fs::remove_dir_all(&root); } /// Copy takes images only: with copy, a call that includes a placed document fails and copies /// nothing; without it, the document is relinked where its file is. #[test] fn copy_refuses_placed_documents() { - let root = folder("copy-placed"); + let (_dir, root) = folder("copy-placed"); let mut s = session_for(&root); s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); let badge = root.join("badge.vectorcraft"); @@ -471,7 +467,7 @@ fn copy_refuses_placed_documents() { #[cfg(unix)] #[test] fn a_copy_stays_inside_the_write_root() { - let root = folder("roots"); + let (_dir, root) = folder("roots"); let mut s = session_for(&root); write(&root.join("orig/photo.png"), &png(40, 30, RED)); let id = placed(&mut s, &root.join("orig/photo.png")); @@ -487,14 +483,13 @@ fn a_copy_stays_inside_the_write_root() { assert!(matches!(&r, Err(EngineError::Other(m)) if m.contains("outside the write root")), "{r:?}"); assert!(!root.join("doc/photo 2.png").exists()); assert_eq!(std::fs::read(allowed.join("photo.png")).unwrap(), b"kept as it is"); - let _ = std::fs::remove_dir_all(&root); } /// An SVG's linked files are read on the importer's own thread (it needs a deep stack): the roots /// in force on the thread that opens the SVG hold there too. #[test] fn an_svg_reads_its_links_within_the_read_root() { - let root = folder("svg-roots"); + let (_dir, root) = folder("svg-roots"); for dir in ["in", "out"] { write(&root.join(dir).join("dot.png"), &png(4, 4, RED)); } @@ -513,12 +508,11 @@ fn an_svg_reads_its_links_within_the_read_root() { assert_eq!(import("in"), inside, "inside the read root: read as before"); assert_ne!(import("out"), outside, "outside the read root: not read"); }); - let _ = std::fs::remove_dir_all(&root); } #[test] fn junk_search_params_never_fail_internally() { - let root = folder("junk"); + let (_dir, root) = folder("junk"); let mut s = session_for(&root); write(&root.join("orig/a.png"), &png(4, 4, RED)); let id = placed(&mut s, &root.join("orig/a.png")); diff --git a/crates/engine/src/tests/shortcutset.rs b/crates/engine/src/tests/shortcutset.rs index 8f216b8c00..fb5292e243 100644 --- a/crates/engine/src/tests/shortcutset.rs +++ b/crates/engine/src/tests/shortcutset.rs @@ -51,7 +51,7 @@ fn entries_keep_their_sections_and_the_command_counts_codes() { assert_eq!(r["codes"]["modifiers"], json!([{"value": 64, "count": 2}, {"value": 80, "count": 1}])); assert_eq!((r["unread"].as_array().unwrap().len(), r["truncated"].as_bool()), (0, Some(false)), "{r}"); // The same from a file, and from its bytes. - let dir = vectorcraft_testkit::temp_dir("shortcutset-read"); + let dir = vectorcraft_testkit::TempDir::new("shortcutset-read"); let path = dir.join("Studio Keys.kys"); std::fs::write(&path, text).unwrap(); let mut s = Session::new(); @@ -59,7 +59,6 @@ fn entries_keep_their_sections_and_the_command_counts_codes() { assert_eq!((r["set"].as_str(), r["entries"].as_array().unwrap().len()), (Some("Studio Keys"), 3)); let b64 = vectorcraft_format::base64_encode(text.as_bytes()); assert_eq!(s.execute("shortcutSet.read", &json!({ "dataBase64": b64 })).unwrap()["entries"].as_array().unwrap().len(), 3); - let _ = std::fs::remove_dir_all(dir); } #[test] @@ -124,7 +123,7 @@ fn caps_nesting_entries_and_names() { #[test] fn big_files_are_read_up_to_the_cap() { let text = format!("/Menus {{ /E {{ /Key 1 }} {} }}", " ".repeat(5 << 20)); - let dir = vectorcraft_testkit::temp_dir("shortcutset-big"); + let dir = vectorcraft_testkit::TempDir::new("shortcutset-big"); let path = dir.join("Big.kys"); std::fs::write(&path, &text).unwrap(); let b64 = vectorcraft_format::base64_encode(text.as_bytes()); @@ -145,7 +144,6 @@ fn big_files_are_read_up_to_the_cap() { assert_eq!(fits.len(), (4 << 20) + 3); std::fs::write(&path, &fits).unwrap(); assert_eq!(s.execute("shortcutSet.read", &json!({ "path": path.to_string_lossy() })).unwrap()["truncated"], false); - let _ = std::fs::remove_dir_all(dir); } fn matched(s: &mut Session, p: Value) -> Vec { diff --git a/crates/engine/src/tests/spelling.rs b/crates/engine/src/tests/spelling.rs index 16d7f01993..dd10df1597 100644 --- a/crates/engine/src/tests/spelling.rs +++ b/crates/engine/src/tests/spelling.rs @@ -161,7 +161,7 @@ fn a_changed_word_replaces_the_old_one() { #[test] fn the_custom_dictionary_file_is_read_and_kept_up_to_date() { let _one_at_a_time = dictionary_lock(); - let dir = vectorcraft_testkit::temp_dir("spelling-dictionary"); + let dir = vectorcraft_testkit::TempDir::new("spelling-dictionary"); let file = dir.join("Custom Dictionary.txt"); std::fs::write(&file, "kqzvwx\nnot one word\n\n").unwrap(); let mut s = session(); @@ -224,7 +224,7 @@ fn in_dictionary(s: &mut Session, word: &str) -> bool { #[test] fn a_dictionary_file_that_cant_be_read_whole_is_never_written_over() { let _one_at_a_time = dictionary_lock(); - let dir = vectorcraft_testkit::temp_dir("spelling-unreadable"); + let dir = vectorcraft_testkit::TempDir::new("spelling-unreadable"); // A folder where the file should be can't be read as one; a file past what craft-proof reads // would lose its other words if written back. let folder = dir.join("Custom Dictionary.txt"); @@ -248,7 +248,7 @@ fn a_change_that_cant_be_saved_is_taken_back() { let _one_at_a_time = dictionary_lock(); let mut s = session(); run(&mut s, "text.spelling.addWord", json!({"word": "wkqzvx"})); - let dir = vectorcraft_testkit::temp_dir("spelling-rollback"); + let dir = vectorcraft_testkit::TempDir::new("spelling-rollback"); let folder = dir.join("Custom Dictionary.txt"); std::fs::create_dir_all(&folder).unwrap(); let mut failing = session(); @@ -266,7 +266,7 @@ fn a_change_that_cant_be_saved_is_taken_back() { fn a_dictionary_edit_is_saved_whole_or_not_at_all() { let _one_at_a_time = dictionary_lock(); const EDIT: &str = "text.spelling.editDictionary"; - let dir = vectorcraft_testkit::temp_dir("spelling-edit"); + let dir = vectorcraft_testkit::TempDir::new("spelling-edit"); let file = dir.join("Custom Dictionary.txt"); std::fs::write(&file, "pqzvka\npqzvkb\n").unwrap(); let mut s = session(); @@ -302,7 +302,7 @@ fn a_dictionary_edit_is_saved_whole_or_not_at_all() { fn a_word_another_window_saved_stays_when_this_save_fails() { let _one_at_a_time = dictionary_lock(); use std::os::unix::fs::PermissionsExt; - let dir = vectorcraft_testkit::temp_dir("spelling-readonly"); + let dir = vectorcraft_testkit::TempDir::new("spelling-readonly"); let file = dir.join("Custom Dictionary.txt"); std::fs::write(&file, "").unwrap(); let mut s = session(); diff --git a/crates/engine/src/tests/store.rs b/crates/engine/src/tests/store.rs index b6ac7b0201..21967473bb 100644 --- a/crates/engine/src/tests/store.rs +++ b/crates/engine/src/tests/store.rs @@ -7,10 +7,8 @@ use vectorcraft_color::cms; use crate::Session; use crate::store::{MAX_PRESET_FILE, file_stem, holds}; -fn fresh_dir(tag: &str) -> std::path::PathBuf { - let dir = vectorcraft_testkit::temp_dir(&format!("store-{tag}")); - let _ = std::fs::remove_dir_all(&dir); - dir +fn fresh_dir(tag: &str) -> vectorcraft_testkit::TempDir { + vectorcraft_testkit::TempDir::new(&format!("store-{tag}")) } fn saved_names(s: &mut Session) -> Vec { @@ -20,7 +18,8 @@ fn saved_names(s: &mut Session) -> Vec { #[test] fn saved_presets_come_back_in_the_next_session_and_renames_and_deletes_follow() { - let dir = fresh_dir("presets").join("PDF Presets"); + let root = fresh_dir("presets"); + let dir = root.join("PDF Presets"); let mut a = Session::new(); a.attach_pdf_preset_store(dir.clone()); a.execute("pdf.preset.save", &json!({"name": "Press", "description": "for the printer", "preset": "Press Quality"})).unwrap(); @@ -42,7 +41,8 @@ fn saved_presets_come_back_in_the_next_session_and_renames_and_deletes_follow() #[test] fn a_rename_that_cannot_be_written_keeps_the_preset_under_its_old_name() { - let dir = fresh_dir("rename-fails").join("PDF Presets"); + let root = fresh_dir("rename-fails"); + let dir = root.join("PDF Presets"); let mut a = Session::new(); a.attach_pdf_preset_store(dir.clone()); a.execute("pdf.preset.save", &json!({"name": "Web", "description": "for screens"})).unwrap(); @@ -59,7 +59,8 @@ fn a_rename_that_cannot_be_written_keeps_the_preset_under_its_old_name() { #[test] fn a_rename_that_only_changes_case_keeps_one_file() { - let dir = fresh_dir("rename-case").join("PDF Presets"); + let root = fresh_dir("rename-case"); + let dir = root.join("PDF Presets"); let mut a = Session::new(); a.attach_pdf_preset_store(dir.clone()); a.execute("pdf.preset.save", &json!({"name": "Web"})).unwrap(); @@ -80,7 +81,8 @@ fn file_names(dir: &std::path::Path) -> Vec { #[cfg(windows)] #[test] fn a_rename_that_only_changes_case_and_cannot_be_written_keeps_the_old_file() { - let dir = fresh_dir("rename-case-fails").join("PDF Presets"); + let root = fresh_dir("rename-case-fails"); + let dir = root.join("PDF Presets"); let mut a = Session::new(); a.attach_pdf_preset_store(dir.clone()); a.execute("pdf.preset.save", &json!({"name": "Web", "description": "for screens"})).unwrap(); @@ -103,7 +105,8 @@ fn a_rename_that_only_changes_case_and_cannot_be_written_keeps_the_old_file() { #[cfg(not(any(windows, target_vendor = "apple")))] #[test] fn a_rename_that_only_changes_case_and_cannot_be_moved_keeps_the_old_preset() { - let dir = fresh_dir("rename-case-blocked").join("PDF Presets"); + let root = fresh_dir("rename-case-blocked"); + let dir = root.join("PDF Presets"); let mut a = Session::new(); a.attach_pdf_preset_store(dir.clone()); a.execute("pdf.preset.save", &json!({"name": "Web", "description": "for screens"})).unwrap(); @@ -118,7 +121,8 @@ fn a_rename_that_only_changes_case_and_cannot_be_moved_keeps_the_old_preset() { #[test] fn a_folder_that_does_not_exist_yet_takes_the_presets_the_preferences_held() { - let dir = fresh_dir("seed").join("PDF Presets"); + let root = fresh_dir("seed"); + let dir = root.join("PDF Presets"); let mut a = Session::new(); a.execute("pdf.preset.save", &json!({"name": "From Preferences"})).unwrap(); a.attach_pdf_preset_store(dir.clone()); @@ -155,7 +159,8 @@ fn a_write_reads_the_file_it_replaces_only_one_byte_past_the_new_length() { #[test] fn a_saved_preset_replaces_an_oversized_file_in_its_place() { - let dir = fresh_dir("oversized-put").join("PDF Presets"); + let root = fresh_dir("oversized-put"); + let dir = root.join("PDF Presets"); let mut a = Session::new(); a.attach_pdf_preset_store(dir.clone()); a.execute("pdf.preset.save", &json!({"name": "Web"})).unwrap(); @@ -171,7 +176,8 @@ fn a_saved_preset_replaces_an_oversized_file_in_its_place() { #[test] fn damaged_unsafe_and_oversized_preset_files_are_skipped() { - let dir = fresh_dir("damaged").join("PDF Presets"); + let root = fresh_dir("damaged"); + let dir = root.join("PDF Presets"); let mut a = Session::new(); a.attach_pdf_preset_store(dir.clone()); a.execute("pdf.preset.save", &json!({"name": "Good"})).unwrap(); @@ -214,7 +220,8 @@ fn icc_named(name: &str) -> Vec { #[test] fn stored_profiles_register_again_and_removing_one_removes_its_copy() { - let dir = fresh_dir("profiles").join("Color Profiles"); + let root = fresh_dir("profiles"); + let dir = root.join("Color Profiles"); std::fs::create_dir_all(&dir).unwrap(); let name = "StoreRGB-1"; let file = format!("{}.icc", file_stem(name)); @@ -229,7 +236,8 @@ fn stored_profiles_register_again_and_removing_one_removes_its_copy() { // A profile loaded from elsewhere is copied in, whatever the file is called. let other = "StoreRGB-2"; - let loose = vectorcraft_testkit::temp_dir("store-loose").join("whatever.icm"); + let loose_dir = vectorcraft_testkit::TempDir::new("store-loose"); + let loose = loose_dir.join("whatever.icm"); std::fs::write(&loose, icc_named(other)).unwrap(); let r = s.execute("color.loadProfile", &json!({"path": loose.to_str().unwrap()})).unwrap(); assert_eq!((r["name"].as_str(), r["remembered"].as_bool()), (Some(other), Some(true))); diff --git a/crates/engine/src/tests/swatchlib.rs b/crates/engine/src/tests/swatchlib.rs index b380a4e06b..445ce38ea7 100644 --- a/crates/engine/src/tests/swatchlib.rs +++ b/crates/engine/src/tests/swatchlib.rs @@ -130,11 +130,9 @@ fn reset_defaults_restores_missing_swatches_or_replaces_them() { assert_eq!(s.paint.fill, Paint::solid(Color::from_hex("#abcdef").unwrap()), "the default fill is unlinked too"); } -/// A fresh scratch folder for one test. -fn temp_dir(name: &str) -> std::path::PathBuf { - let d = std::env::temp_dir().join(format!("vc-swatchlib-{name}-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&d); - d +/// A fresh scratch folder for one test, removed when the returned value is dropped. +fn temp_dir(name: &str) -> vectorcraft_testkit::TempDir { + vectorcraft_testkit::TempDir::new(&format!("swatchlib-{name}")) } #[test] diff --git a/crates/engine/src/tests/webexport.rs b/crates/engine/src/tests/webexport.rs index bdd474612f..cf427f8174 100644 --- a/crates/engine/src/tests/webexport.rs +++ b/crates/engine/src/tests/webexport.rs @@ -294,7 +294,7 @@ fn files_named_after_the_title_follow_windows_naming_rules() { #[test] fn files_are_written_beside_the_path() { - let dir = vectorcraft_testkit::temp_dir("webexport"); + let dir = vectorcraft_testkit::TempDir::new("webexport"); let mut s = two_colours(); let path = dir.join("art.gif").to_string_lossy().to_string(); let r = run(&mut s, "document.exportForWeb", json!({"path": path})); @@ -324,7 +324,7 @@ fn an_export_says_when_an_image_could_not_be_decoded() { let r = run(&mut s, "document.exportForWeb.preview", json!({"format": format})); assert!(undecoded(&r), "{format} preview: {}", r["warnings"]); } - let dir = vectorcraft_testkit::temp_dir("webexport-undecoded"); + let dir = vectorcraft_testkit::TempDir::new("webexport-undecoded"); let page = dir.join("page.html").to_string_lossy().to_string(); let r = run(&mut s, "document.exportForWeb", json!({"path": page, "output": "html"})); assert!(undecoded(&r), "{r}"); diff --git a/crates/engine/tests/color_settings.rs b/crates/engine/tests/color_settings.rs index fe56377d4e..e8b3ab0906 100644 --- a/crates/engine/tests/color_settings.rs +++ b/crates/engine/tests/color_settings.rs @@ -653,7 +653,7 @@ fn loading_the_working_rgb_profile_again_takes_effect_at_once() { bytes }; let name = "Reload RGB"; - let dir = vectorcraft_testkit::temp_dir("reload-working-rgb"); + let dir = vectorcraft_testkit::TempDir::new("reload-working-rgb"); let load = |s: &mut Session, builtin: &str| { let path = dir.join(format!("{}.icc", builtin.len())); std::fs::write(&path, renamed(builtin, name)).unwrap(); @@ -734,11 +734,9 @@ impl Drop for ProofReset { } } -/// A folder of its own for the test `tag`, empty. -fn fresh_dir(tag: &str) -> std::path::PathBuf { - let dir = vectorcraft_testkit::temp_dir(&format!("color-settings-{tag}")); - let _ = std::fs::remove_dir_all(&dir); - dir +/// A folder of its own for the test `tag`, empty, removed when dropped. +fn fresh_dir(tag: &str) -> vectorcraft_testkit::TempDir { + vectorcraft_testkit::TempDir::new(&format!("color-settings-{tag}")) } /// An ICC file of an RGB profile named `name` (a built-in one with its description replaced by a @@ -785,7 +783,8 @@ fn color_settings_come_back_in_the_next_session() { let root = fresh_dir("color-settings"); let (profiles, file) = (root.join("Color Profiles"), root.join("Color Settings.json")); let loaded = "StoreRGB-6"; - let icc = vectorcraft_testkit::temp_dir("store-color-settings").join("loaded.icc"); + let icc_dir = vectorcraft_testkit::TempDir::new("store-color-settings"); + let icc = icc_dir.join("loaded.icc"); let _reset = ColorSettingsReset { profile: Some(loaded), paths: vec![root.clone(), icc.clone()] }; std::fs::write(&icc, icc_named(loaded)).unwrap(); diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 6577754de0..d88d99665f 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -12,10 +12,10 @@ use vectorcraft_testkit::invariants::{check_all, check_session, doc_json}; use vectorcraft_testkit::strategies::{command_param_keys, junk_params_for_command, junk_values, param_keys, safe_value_for_command}; /// Commands that replace/close the active document or touch the filesystem are fine to call, but -/// their file and folder params are redirected into a temp dir (`safe_value_for_command`, which -/// `junk_params_for_command` applies, by the keys the command catalog names as files and folders). -fn safe_path() -> String { - vectorcraft_testkit::temp_dir("sweep").join("out.bin").to_string_lossy().to_string() +/// their file and folder params are redirected into the test's temp dir `dir` (`safe_value_for_command`, +/// which `junk_params_for_command` applies, by the keys the command catalog names as files and folders). +fn safe_path(dir: &std::path::Path) -> String { + dir.join("out.bin").to_string_lossy().to_string() } /// `r`, after failing the case if the command panicked. The engine's guard reports a panic as @@ -219,7 +219,8 @@ fn junk_cases(c: &vectorcraft_engine::CommandSpec, path: &str) -> Vec { #[test] fn every_command_with_junk_params() { - let path = safe_path(); + let dir = vectorcraft_testkit::TempDir::new("sweep"); + let path = safe_path(&dir); let mut failures = vec![]; let mut calls = 0usize; for fx in Fixture::ALL { @@ -311,8 +312,8 @@ fn names_location(spec: Option<&vectorcraft_engine::CommandSpec>, key: &str) -> /// command alone in at least one case ([`junk_cases`]). #[test] fn sweep_cases_name_files_only_inside_the_temp_folder() { - let path = safe_path(); - let dir = vectorcraft_testkit::temp_dir("sweep"); + let dir = vectorcraft_testkit::TempDir::new("sweep"); + let path = safe_path(&dir); let mut unsent = vec![]; let mut outside = std::collections::BTreeSet::new(); for c in command_specs() { @@ -344,8 +345,8 @@ fn sweep_cases_name_files_only_inside_the_temp_folder() { /// `paths: [{ids, path}]`, each with a string in the temp folder, and a folder key a folder there. #[test] fn sweep_cases_send_the_file_keys_of_the_command_catalog() { - let path = safe_path(); - let dir = vectorcraft_testkit::temp_dir("sweep"); + let dir = vectorcraft_testkit::TempDir::new("sweep"); + let path = safe_path(&dir); let spec = |id: &str| command_specs().iter().find(|c| c.id == id).unwrap(); let inside = |v: &Value| v.as_str().is_some_and(|s| std::path::Path::new(s).starts_with(&dir)); for (id, key) in [ @@ -381,7 +382,8 @@ fn sweep_cases_send_the_file_keys_of_the_command_catalog() { /// document as it was, still applies after it. #[test] fn commands_that_do_not_wait_for_a_tool_edit_change_no_document() { - let path = safe_path(); + let dir = vectorcraft_testkit::TempDir::new("sweep"); + let path = safe_path(&dir); let mut failures = vec![]; for fx in [Fixture::Single, Fixture::Multi] { for c in command_specs().iter().filter(|c| !vectorcraft_engine::waits_for_tool_edit(c.id)) { @@ -1127,7 +1129,8 @@ fn serializable_cases(c: &vectorcraft_engine::CommandSpec, path: &str) -> Vec Value { #[test] fn a_found_font_is_copied_without_replacing_a_file_and_its_type_redraws() { - let root = vectorcraft_testkit::temp_dir("font-folder"); - let _ = std::fs::remove_dir_all(&root); - std::fs::create_dir_all(root.join("Downloads/Deep")).unwrap(); - let root = plain(std::fs::canonicalize(&root).unwrap()); + let dir = vectorcraft_testkit::TempDir::new("font-folder"); + std::fs::create_dir_all(dir.join("Downloads/Deep")).unwrap(); + let root = plain(std::fs::canonicalize(&*dir).unwrap()); let fonts = root.join("App Fonts"); vectorcraft_text::set_app_font_dir(fonts.clone()); assert!(vectorcraft_text::system_font_dirs().contains(&fonts)); @@ -85,5 +84,4 @@ fn a_found_font_is_copied_without_replacing_a_file_and_its_type_redraws() { // Only files the last search found are copied. let r = s.execute("text.addFontFiles", &json!({"files": [root.join("other.ttf")]})); assert!(r.as_ref().is_err_and(|e| e.to_string().contains("the last text.findFontFiles search")), "{r:?}"); - let _ = std::fs::remove_dir_all(&root); } diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 37f8c63acf..04e7094f9e 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -6142,7 +6142,8 @@ fn findme_wanted() -> vectorcraft_text::WantedFonts { /// Whether reading `bytes` as a font file a search reads panics. fn search_reads(what: &str, bytes: &[u8]) -> Result, TestCaseError> { - let path = vectorcraft_testkit::temp_dir("font-search-fuzz").join(format!("{:?}.ttc", std::thread::current().id())); + let dir = vectorcraft_testkit::TempDir::new("font-search-fuzz"); + let path = dir.join("font.ttc"); std::fs::write(&path, bytes).unwrap(); catch_quiet(|| findme_wanted().provided_by(&path)).map_err(|e| TestCaseError::fail(format!("{what}: a search reading the file panicked: {e}"))) } diff --git a/crates/engine/tests/it/model_based.rs b/crates/engine/tests/it/model_based.rs index b11e3b0591..583dfda441 100644 --- a/crates/engine/tests/it/model_based.rs +++ b/crates/engine/tests/it/model_based.rs @@ -134,7 +134,8 @@ fn a_failed_batch_restores_the_file_the_history_and_a_drag_in_progress() { (doc_json(&st.doc), st.history.undo.len(), st.history.redo.len()) }; // Save As, then a failing step: the document keeps no file and stays modified. - let path = vectorcraft_testkit::temp_dir("batch-rollback").join("named.vectorcraft").to_string_lossy().to_string(); + let dir = vectorcraft_testkit::TempDir::new("batch-rollback"); + let path = dir.join("named.vectorcraft").to_string_lossy().to_string(); let save = json!({"command": "file.saveAs", "params": {"path": path}}); assert!(s.execute("command.batch", &json!({"commands": [rect(0), save, fail]})).is_err()); assert_eq!(state(&s), before); @@ -170,7 +171,7 @@ fn empty_batch_is_harmless() { #[test] fn save_open_via_commands_roundtrips() { let mut s = fixtures::rich_session(); - let dir = vectorcraft_testkit::temp_dir("engine-save"); + let dir = vectorcraft_testkit::TempDir::new("engine-save"); let path = dir.join("rich.vectorcraft"); exec(&mut s, "document.save", json!({"path": path.to_str().unwrap()})); let want = doc_json(&s.doc().unwrap().doc); diff --git a/crates/engine/tests/removed_font.rs b/crates/engine/tests/removed_font.rs index ca5dc1f13a..da885b40b8 100644 --- a/crates/engine/tests/removed_font.rs +++ b/crates/engine/tests/removed_font.rs @@ -37,9 +37,7 @@ fn laid_out_in(s: &Session, id: u64) -> Vec { #[test] fn type_in_a_removed_font_falls_back_after_a_rescan() { - let root = vectorcraft_testkit::temp_dir("removed-font"); - let _ = std::fs::remove_dir_all(&root); - std::fs::create_dir_all(&root).unwrap(); + let root = vectorcraft_testkit::TempDir::new("removed-font"); vectorcraft_text::set_user_font_dirs(vec![root.clone()]); let file = root.join("Gonefont.otf"); std::fs::write(&file, vectorcraft_testkit::fonts::renamed(FAMILY)).unwrap(); @@ -72,5 +70,4 @@ fn type_in_a_removed_font_falls_back_after_a_rescan() { assert_eq!(status(&mut s), ("exact".into(), FAMILY.into())); assert!(listed(&mut s)); assert_eq!(laid_out_in(&s, id), [FAMILY]); - let _ = std::fs::remove_dir_all(&root); } diff --git a/crates/testkit/src/lib.rs b/crates/testkit/src/lib.rs index cecaba6b13..fa7b39aa9d 100644 --- a/crates/testkit/src/lib.rs +++ b/crates/testkit/src/lib.rs @@ -85,6 +85,41 @@ pub fn temp_dir(tag: &str) -> std::path::PathBuf { dir } +/// A test's own temporary directory, removed with everything in it when dropped, also when the +/// test panics. [`temp_dir`] makes it, with the same checks, under a tag no other `TempDir` in this +/// process uses, so tests running alongside never share one. A removal that fails (a file still +/// held open) is ignored; the next process with the same id empties the directory before use. +pub struct TempDir(std::path::PathBuf); + +impl TempDir { + /// A new empty directory `vectorcraft-testkit---` in the system temporary + /// folder, where `n` counts the `TempDir`s made in this process. + pub fn new(tag: &str) -> Self { + static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + Self(temp_dir(&format!("{tag}-{n}"))) + } +} + +impl std::ops::Deref for TempDir { + type Target = std::path::PathBuf; + fn deref(&self) -> &std::path::PathBuf { + &self.0 + } +} + +impl AsRef for TempDir { + fn as_ref(&self) -> &std::path::Path { + &self.0 + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + /// How many shapes an SVG draws: its `` elements and the basic shapes SVG export writes /// simple shapes as (``, ``, ``, ``, ``, ``). pub fn svg_shapes(svg: &str) -> usize { @@ -134,4 +169,40 @@ mod tests { assert!(super::temp_dir(tag).join("mine.txt").is_file(), "a later call keeps this process's files"); std::fs::remove_dir_all(&dir).unwrap(); } + + #[test] + fn a_test_dir_is_removed_with_its_contents_when_dropped() { + let dir = super::TempDir::new("dropped"); + let path = dir.to_path_buf(); + std::fs::create_dir_all(path.join("nested")).unwrap(); + std::fs::write(path.join("nested").join("a.txt"), b"a").unwrap(); + drop(dir); + assert!(!path.exists(), "{} is left", path.display()); + } + + #[test] + fn a_test_dir_is_removed_when_its_test_panics() { + let mut path = None; + let r = super::catch_quiet::<()>(|| { + let dir = super::TempDir::new("panicked"); + std::fs::write(dir.join("a.txt"), b"a").unwrap(); + path = Some(dir.to_path_buf()); + panic!("the test fails"); + }); + assert_eq!(r.err().as_deref(), Some("the test fails")); + let path = path.unwrap(); + assert!(!path.exists(), "{} is left", path.display()); + } + + #[test] + fn test_dirs_with_one_tag_are_separate_and_private() { + let (a, b) = (super::TempDir::new("shared-tag"), super::TempDir::new("shared-tag")); + assert_ne!(*a, *b); + std::fs::write(a.join("a.txt"), b"a").unwrap(); + assert_eq!(std::fs::read_dir(&*b).unwrap().count(), 0, "one test's files are not in another's directory"); + #[cfg(unix)] + assert_eq!(std::os::unix::fs::PermissionsExt::mode(&std::fs::metadata(&*a).unwrap().permissions()) & 0o777, 0o700); + drop(a); + assert!(b.is_dir(), "dropping one leaves the other"); + } }