From 07522fb4e6b92f0d470ebd0d94ac6f2831ae8c37 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 07:01:50 -0800 Subject: [PATCH] Tests: ui-egui tests remove the temp folders they make Place, taskbar, opening, Save Options, Missing Fonts and one control test left their folders in the system temp folder after every run. Other opening and control tests removed theirs only at the end of a passing test, so a failing one left its folder. - A test-only Folder guard (crate::tests_folder) removes its folder with everything in it when dropped, also when the test panics, as the find-files tests' Tree guard does (#6948). - The opening and control tests that made their own folders use it. - Each Place test gets its own folder instead of one shared per-process folder, so one test's cleanup cannot remove another's files. - The Missing Fonts helper no longer removes and remakes the folder vectorcraft_testkit::temp_dir made, which keeps it owner-only on Unix. - vectorcraft_testkit::temp_dir is unchanged. --- crates/ui-egui/src/control/tests.rs | 35 +++---- .../src/dialogs/missing_fonts/tests.rs | 16 ++-- .../ui-egui/src/dialogs/save_options/tests.rs | 11 ++- crates/ui-egui/src/lib.rs | 4 +- crates/ui-egui/src/opening/tests.rs | 33 ++----- crates/ui-egui/src/tests.rs | 1 + crates/ui-egui/src/tests/folder.rs | 76 +++++++++++++++ crates/ui-egui/src/tests/place.rs | 93 +++++++++++-------- crates/ui-egui/src/tests/taskbar.rs | 8 +- 9 files changed, 171 insertions(+), 106 deletions(-) create mode 100644 crates/ui-egui/src/tests/folder.rs diff --git a/crates/ui-egui/src/control/tests.rs b/crates/ui-egui/src/control/tests.rs index 19751bd3cd..ac8fbfa3eb 100644 --- a/crates/ui-egui/src/control/tests.rs +++ b/crates/ui-egui/src/control/tests.rs @@ -33,11 +33,9 @@ fn hiding(hidden: &[(&str, &str)], readable: &[&str]) -> super::Hiding { /// [`super::hide_library_ids`] on `reply`, under a read root made for `tag`. fn hide_under_a_root(tag: &str, reply: &mut Value, hidden: &super::Hiding) { - let root = std::env::temp_dir().join(format!("vc-{tag}-root-{}", std::process::id())); - std::fs::create_dir_all(&root).unwrap(); - let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap(); + let root = crate::tests_folder::Folder::new(&format!("{tag}-root")); + let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[root.as_path()], &[]).unwrap(); vectorcraft_engine::file_access::confine(roots.as_ref(), || super::hide_library_ids(reply, hidden)); - let _ = std::fs::remove_dir_all(root); } /// Encoded file data in a reply, a base64 string under `dataBase64`, `pngBase64`, `thumbnailBase64` @@ -73,7 +71,7 @@ fn a_reply_keeps_encoded_data_that_holds_a_hidden_path() { /// space do not keep it. #[test] fn a_reply_keeps_a_hidden_path_only_inside_a_known_library_id() { - let base = std::env::temp_dir().join(format!("vc-continuation-{}", std::process::id())); + let base = crate::tests_folder::Folder::new("continuation"); let root = base.join("in"); std::fs::create_dir_all(&root).unwrap(); let hidden_path = base.join("in x").join("b.svg").to_string_lossy().into_owned(); @@ -84,7 +82,6 @@ fn a_reply_keeps_a_hidden_path_only_inside_a_known_library_id() { let mut reply = json!({"error": format!("kept {hidden_path}.bak, {other}{up} and {known}")}); let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap(); vectorcraft_engine::file_access::confine(roots.as_ref(), || super::hide_library_ids(&mut reply, &hidden)); - let _ = std::fs::remove_dir_all(base); assert_eq!(reply["error"], json!(format!("kept b.svg.bak, b.svg (2){up} and {known}")), "{reply}"); // A readable library's id with a space after the hidden part stays whole. // A readable id with a `..` part does not keep the hidden part. @@ -250,7 +247,7 @@ fn a_reply_hides_a_hidden_relative_path_written_as_absolute() { /// by their file names; plain text and a path inside the root are not. #[test] fn a_request_sends_paths_that_an_error_hides() { - let base = std::env::temp_dir().join(format!("vc-sent-paths-{}", std::process::id())); + let base = crate::tests_folder::Folder::new("sent-paths"); let root = base.join("in"); std::fs::create_dir_all(&root).unwrap(); let inside = root.join("a.svg").to_string_lossy().into_owned(); @@ -259,7 +256,6 @@ fn a_request_sends_paths_that_an_error_hides() { let mut sent = Vec::new(); let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap(); vectorcraft_engine::file_access::confine(roots.as_ref(), || super::sent_paths(¶ms, &mut sent)); - let _ = std::fs::remove_dir_all(base); let pair = |path: &str, name: &str| (format!("loaded/{path}"), format!("loaded/{name}")); let want = [ pair("outside/h.svg", "h.svg"), @@ -276,7 +272,7 @@ fn a_request_sends_paths_that_an_error_hides() { /// relative name and a path inside the root are not. #[test] fn an_absolute_path_outside_the_read_root_is_unreadable() { - let base = std::env::temp_dir().join(format!("vc-unreadable-absolute-{}", std::process::id())); + let base = crate::tests_folder::Folder::new("unreadable-absolute"); std::fs::create_dir_all(base.join("in")).unwrap(); let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&base.join("in")], &[]).unwrap(); let inside = base.join("in").join("a.svg").to_string_lossy().into_owned(); @@ -295,7 +291,6 @@ fn an_absolute_path_outside_the_read_root_is_unreadable() { assert!(!super::unreadable_absolute("keep")); assert!(!super::unreadable_absolute(&inside)); }); - let _ = std::fs::remove_dir_all(base); } /// Without a read root, a `file:` URL that names another host may be read, and a recorded step keeps @@ -395,12 +390,10 @@ fn a_reply_keeps_a_longer_path_that_ends_with_a_hidden_relative_path() { #[cfg(not(windows))] #[test] fn a_sent_path_keeps_a_backslash_in_its_file_name() { - let root = std::env::temp_dir().join(format!("vc-sent-backslash-{}", std::process::id())); - std::fs::create_dir_all(&root).unwrap(); - let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap(); + let root = crate::tests_folder::Folder::new("sent-backslash"); + let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[root.as_path()], &[]).unwrap(); let mut sent = Vec::new(); vectorcraft_engine::file_access::confine(roots.as_ref(), || super::sent_paths(&json!({"path": "/out/weird\\name.svg"}), &mut sent)); - let _ = std::fs::remove_dir_all(root); assert_eq!(sent, [("loaded//out/weird\\name.svg".to_string(), "loaded/weird\\name.svg".to_string())]); } @@ -412,13 +405,11 @@ fn a_hidden_library_at_the_top_of_the_file_system_keeps_its_shown_id() { let panel = "library:swatches:loaded//b.svg".to_string(); app.ui.color_guide_limit = "loaded//b.svg".to_string(); app.ui.open_panel = Some(panel); - let root = std::env::temp_dir().join(format!("vc-top-level-library-{}", std::process::id())); - std::fs::create_dir_all(&root).unwrap(); - let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap(); + let root = crate::tests_folder::Folder::new("top-level-library"); + let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[root.as_path()], &[]).unwrap(); let (inspected, docking) = vectorcraft_engine::file_access::confine(roots.as_ref(), || { (input_request(&mut app, "ui.inspect", json!({})), input_request(&mut app, "ui.docking", json!({}))) }); - let _ = std::fs::remove_dir_all(root); assert_eq!(inspected["result"]["ui"]["color_guide_limit"], "loaded/b.svg", "{inspected}"); assert_eq!(inspected["result"]["ui"]["open_panel"], "library:swatches:loaded/b.svg", "{inspected}"); assert_eq!(docking["result"]["flyout"], "library:swatches:loaded/b.svg", "{docking}"); @@ -429,9 +420,8 @@ fn a_hidden_library_at_the_top_of_the_file_system_keeps_its_shown_id() { #[test] fn a_sent_path_reads_the_same_whatever_the_outcome() { let app = VectorcraftApp::new(Session::new(), Services::default()); - let root = std::env::temp_dir().join(format!("vc-sent-outcome-{}", std::process::id())); - std::fs::create_dir_all(&root).unwrap(); - let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[&root], &[]).unwrap(); + let root = crate::tests_folder::Folder::new("sent-outcome"); + let roots = vectorcraft_engine::file_access::AutomationRoots::new(&[root.as_path()], &[]).unwrap(); let sent = json!({"path": "/outside/b.svg"}); let mut done = json!({"ok": true, "result": {"path": "/outside/b.svg"}}); let mut failed = json!({"ok": false, "error": "can't read /outside/b.svg"}); @@ -439,7 +429,6 @@ fn a_sent_path_reads_the_same_whatever_the_outcome() { super::hide_in_reply(&app, &mut done, &sent, super::Hiding::default()); super::hide_in_reply(&app, &mut failed, &sent, super::Hiding::default()); }); - let _ = std::fs::remove_dir_all(root); assert_eq!(done["result"]["path"], "b.svg", "{done}"); assert_eq!(failed["error"], "can't read b.svg", "{failed}"); } @@ -1157,7 +1146,7 @@ fn panels_that_redraw_apply_a_merge_on_another_thread_first() { /// applies the merge first: the file holds it, and closing asks to save it. #[test] fn save_and_close_apply_a_merge_on_another_thread_first() { - let dir = vectorcraft_testkit::temp_dir("control-merge-save"); + let dir = crate::tests_folder::Folder::adopt(vectorcraft_testkit::temp_dir("control-merge-save")); for then in ["file.saveAs", "file.exportAs", "file.close"] { let services = Services { write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))), ..Default::default() }; let mut app = VectorcraftApp::new(Session::new(), services); diff --git a/crates/ui-egui/src/dialogs/missing_fonts/tests.rs b/crates/ui-egui/src/dialogs/missing_fonts/tests.rs index 0f57b99f46..b173deb49c 100644 --- a/crates/ui-egui/src/dialogs/missing_fonts/tests.rs +++ b/crates/ui-egui/src/dialogs/missing_fonts/tests.rs @@ -1,21 +1,20 @@ -use std::path::{Path, PathBuf}; +use std::path::Path; use std::time::Instant; use vectorcraft_engine::Session; use vectorcraft_engine::cmd::findfiles::Rules; use super::*; +use crate::tests_folder::Folder; fn frame(app: &mut VectorcraftApp) -> String { crate::tests_labels::painted_text(app, |app, ui| super::super::show(app, ui.ctx())) } -/// A fresh temporary folder (canonical, as a search reports it). -fn folder(tag: &str) -> PathBuf { +/// A fresh temporary folder (canonical, as a search reports it), removed when dropped. +fn folder(tag: &str) -> Folder { let dir = vectorcraft_testkit::temp_dir(&format!("missing-fonts-{tag}")); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - vectorcraft_engine::cmd::findfiles::plain(std::fs::canonicalize(&dir).unwrap()) + Folder::adopt(vectorcraft_engine::cmd::findfiles::plain(std::fs::canonicalize(&dir).unwrap())) } /// An app that reads files and whose searches may go anywhere in `root`, on two threads. @@ -191,7 +190,7 @@ fn closing_the_dialog_stops_its_search() { app.session.search_threads = Some(1); app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); app.run("text.create", json!({"x": 10, "y": 40, "text": "Gone", "font": "No Such Font Family"})).unwrap(); - app.run("ui.missingFontsDialog", json!({"folder": root})).unwrap(); + app.run("ui.missingFontsDialog", json!({"folder": root.as_path()})).unwrap(); let id = app.ui.dialog_search.expect("the dialog's search"); super::super::cancel(&mut app); assert!(app.ui.dialog.is_none() && app.ui.dialog_search.is_none()); @@ -275,7 +274,8 @@ fn the_question_comes_back_after_a_dialog_that_took_its_place() { /// opens without the question. #[test] fn generic_families_are_not_asked_about() { - let mut app = app_for(&folder("generic")); + let root = folder("generic"); + let mut app = app_for(&root); let svg = r#"A"#; crate::io::open_bytes(&mut app, "generic.svg", svg.as_bytes(), None).unwrap(); let listed = app.session.execute("text.fonts", &json!({})).unwrap(); diff --git a/crates/ui-egui/src/dialogs/save_options/tests.rs b/crates/ui-egui/src/dialogs/save_options/tests.rs index 968d79568c..7a1aeb3849 100644 --- a/crates/ui-egui/src/dialogs/save_options/tests.rs +++ b/crates/ui-egui/src/dialogs/save_options/tests.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use vectorcraft_engine::Session; use super::*; +use crate::tests_folder::Folder; use crate::{FilePick, Services, theme}; type Written = Rc)>>>; @@ -197,7 +198,7 @@ fn board_files_of(app: &VectorcraftApp, boards: [bool; 4]) -> Vec #[test] fn the_enter_that_closed_the_save_panel_does_not_confirm_the_options() { let (mut app, written, _) = desktop("again.ai"); - let dir = vectorcraft_testkit::temp_dir("ai-options-enter"); + let dir = Folder::adopt(vectorcraft_testkit::temp_dir("ai-options-enter")); let orig = dir.join("orig.ai"); std::fs::write(&orig, vectorcraft_testkit::ai::ai(&vectorcraft_testkit::ai::sample_data(), vectorcraft_testkit::ai::page_pdf())).unwrap(); app.run("document.open", json!({"path": orig.to_string_lossy()})).unwrap(); @@ -215,7 +216,7 @@ fn the_enter_that_closed_the_save_panel_does_not_confirm_the_options() { #[test] fn save_a_copy_and_template_suggest_their_names() { let (mut app, written, picks) = desktop("copy.vectorcraft"); - let folder = vectorcraft_testkit::temp_dir("save-template-dialog"); + let folder = Folder::adopt(vectorcraft_testkit::temp_dir("save-template-dialog")); app.session.prefs.templates_folder = folder.to_string_lossy().into_owned(); app.run("file.saveCopy", json!({})).unwrap(); assert_eq!(picks.borrow()[0].name, "Untitled-1 copy.vectorcraft"); @@ -237,7 +238,8 @@ fn save_a_copy_and_template_suggest_their_names() { #[test] fn save_as_template_writes_ait_when_it_is_picked() { let (mut app, written, _) = desktop("flyer.ait"); - app.session.prefs.templates_folder = vectorcraft_testkit::temp_dir("save-template-ait").to_string_lossy().into_owned(); + let folder = Folder::adopt(vectorcraft_testkit::temp_dir("save-template-ait")); + app.session.prefs.templates_folder = folder.to_string_lossy().into_owned(); app.run("file.saveAsTemplate", json!({})).unwrap(); assert!(app.ui.dialog.is_none(), "a template asks no options"); let (path, bytes) = written.borrow()[0].clone(); @@ -285,9 +287,10 @@ fn the_native_options_have_no_precision_row() { /// replaced with the template's, as Save As replaces the extension of a format it doesn't write. #[test] fn save_as_template_replaces_a_save_formats_extension() { + let folder = Folder::adopt(vectorcraft_testkit::temp_dir("save-template-ext")); for (picked, saved) in [("card.pdf", "card.vctemplate"), ("card.vectorcraft", "card.vctemplate"), ("card.svg", "card.vctemplate")] { let (mut app, written, _) = desktop(picked); - app.session.prefs.templates_folder = vectorcraft_testkit::temp_dir("save-template-ext").to_string_lossy().into_owned(); + app.session.prefs.templates_folder = folder.to_string_lossy().into_owned(); app.run("file.saveAsTemplate", json!({})).unwrap(); let (path, bytes) = written.borrow()[0].clone(); assert_eq!(path, saved, "typed {picked}"); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c4c6ded189..d768854cb4 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -91,8 +91,8 @@ pub mod workspaces; mod document_drag; #[cfg(test)] use tests::{ - background as tests_background, labels as tests_labels, picks as tests_picks, removeanchors as tests_removeanchors, svg as tests_svg, - synthetic as tests_synthetic, sysclip as tests_sysclip, + background as tests_background, folder as tests_folder, labels as tests_labels, picks as tests_picks, removeanchors as tests_removeanchors, + svg as tests_svg, synthetic as tests_synthetic, sysclip as tests_sysclip, }; #[cfg(test)] mod tests; diff --git a/crates/ui-egui/src/opening/tests.rs b/crates/ui-egui/src/opening/tests.rs index ef62509bf3..5840df50cc 100644 --- a/crates/ui-egui/src/opening/tests.rs +++ b/crates/ui-egui/src/opening/tests.rs @@ -56,13 +56,9 @@ fn install_importer(app: &mut VectorcraftApp, src: &str) { } /// A file for the importers below, in a folder of its own. -fn importer_file(tag: &str) -> (std::path::PathBuf, String) { - let dir = std::env::temp_dir().join(format!("vc-ui-opening-{tag}-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - let file = dir.join("a.tob"); - std::fs::write(&file, "not json").unwrap(); - let file = file.to_string_lossy().into_owned(); +fn importer_file(tag: &str) -> (crate::tests_folder::Folder, String) { + let dir = crate::tests_folder::Folder::new(&format!("ui-opening-{tag}")); + let file = dir.file("a.tob", b"not json"); (dir, file) } @@ -73,7 +69,7 @@ fn importer_file(tag: &str) -> (std::path::PathBuf, String) { fn open_with_runs_the_importer_off_the_ui_thread() { use vectorcraft_plugins::wat::{self as tpl, Given}; let rect = r#"{"objects":[{"type":"path","path":{"subpaths":[{"closed":true,"anchors":[{"p":[10,20]},{"p":[110,20]},{"p":[110,70]},{"p":[10,70]}]}]}}]}"#; - let (dir, file) = importer_file("with"); + let (_dir, file) = importer_file("with"); let mut app = VectorcraftApp::new(Session::new(), Services::default()); app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))); let params = r#"{"density":{"type":"number","min":0,"max":10,"default":4}}"#; @@ -97,14 +93,13 @@ fn open_with_runs_the_importer_off_the_ui_thread() { assert!(app.ui.dialog.is_some(), "the failure is reported in a dialog"); assert!(app.ui.recent_files.is_empty()); app.run("plugin.remove", json!({"id": "test.opening.with"})).unwrap(); - let _ = std::fs::remove_dir_all(&dir); } /// Cancel stops an importer that never returns, on the thread reading the file. #[test] fn cancel_stops_a_running_importer() { use vectorcraft_plugins::wat as tpl; - let (dir, file) = importer_file("cancel"); + let (_dir, file) = importer_file("cancel"); let mut app = VectorcraftApp::new(Session::new(), Services::default()); app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))); install_importer(&mut app, &tpl::module(&tpl::importer("test.opening.spins", r#"["tob"]"#, "{}"), "(loop $l (br $l)) (i64.const 0)", "", 1)); @@ -115,7 +110,6 @@ fn cancel_stops_a_running_importer() { assert!(r.as_ref().err().is_some_and(|e| e.contains("cancelled")), "{:?}", r.err()); assert!(app.session.documents().is_empty()); app.run("plugin.remove", json!({"id": "test.opening.spins"})).unwrap(); - let _ = std::fs::remove_dir_all(&dir); } /// #1694: a file that finishes opening while a dialog is up waits for it to close, so the @@ -289,8 +283,7 @@ fn a_file_read_off_the_ui_thread_opens_as_one_read_at_once() { #[test] fn a_file_read_off_the_ui_thread_has_its_links_and_type_as_one_read_at_once() { use image::ImageFormat::{Bmp, Jpeg, Png, Tiff}; - let dir = std::env::temp_dir().join(format!("vc-opening-links-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); + let dir = crate::tests_folder::Folder::new("opening-links"); let (before, after) = (dir.join("before"), dir.join("after")); std::fs::create_dir_all(&before).unwrap(); let file = |folder: &std::path::Path, name: &str| folder.join(name).to_string_lossy().into_owned(); @@ -336,7 +329,6 @@ fn a_file_read_off_the_ui_thread_has_its_links_and_type_as_one_read_at_once() { assert!(st.history.undo.is_empty() && !st.is_dirty(), "{update}"); assert_eq!(asked(&app), question, "{update}"); } - let _ = std::fs::remove_dir_all(&dir); } /// A file opened under automation roots is installed under them too: the open keeps the scope it @@ -344,7 +336,7 @@ fn a_file_read_off_the_ui_thread_has_its_links_and_type_as_one_read_at_once() { #[test] fn a_file_opened_under_automation_roots_finishes_under_them() { use vectorcraft_engine::file_access; - let dir = scope_dir(); + let dir = crate::tests_folder::Folder::new("opening-scope"); let roots = file_access::AutomationRoots::new(&[dir.as_path()], &[]).unwrap(); let mut app = VectorcraftApp::new(Session::new(), Services::default()); file_access::confine(roots.as_ref(), || { @@ -355,12 +347,6 @@ fn a_file_opened_under_automation_roots_finishes_under_them() { assert!(scope.run(file_access::current).is_some(), "the roots go with the open"); } -fn scope_dir() -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("vectorcraft-opening-scope-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - dir -} - /// A file asked for again while it is still being read is not read a second time (#3455). #[test] fn a_file_asked_for_again_while_it_is_read_opens_once() { @@ -378,9 +364,7 @@ fn a_file_asked_for_again_while_it_is_read_opens_once() { /// the dialog acts on its own document (#5369). #[test] fn a_queued_reopen_that_asks_holds_back_files_ready_to_open() { - let dir = std::env::temp_dir().join(format!("vectorcraft-opening-queued-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); + let dir = crate::tests_folder::Folder::new("opening-queued"); let mut app = VectorcraftApp::new(Session::new(), Services::default()); app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))); let (doc, other) = (dir.join("doc.vectorcraft"), dir.join("other.vectorcraft")); @@ -407,7 +391,6 @@ fn a_queued_reopen_that_asks_holds_back_files_ready_to_open() { assert_eq!(app.session.doc().unwrap().path.as_deref(), Some(&*doc), "the dialog's document stays active"); assert_eq!(app.opening.activate.first().map(|(p, _)| p), Some(&other), "the later activation waits"); assert_eq!(app.opening.pending.len(), 1, "the other file waits"); - let _ = std::fs::remove_dir_all(dir); } /// Of two files read off the UI thread and ready in the same frame, the first that opens and asks diff --git a/crates/ui-egui/src/tests.rs b/crates/ui-egui/src/tests.rs index 07d88595d7..b37ed0d10c 100644 --- a/crates/ui-egui/src/tests.rs +++ b/crates/ui-egui/src/tests.rs @@ -8,6 +8,7 @@ mod cut; mod distortkeys; mod docsetup; mod document_drag; +pub(super) mod folder; mod font_menu; mod fonts; mod guide_escape; diff --git a/crates/ui-egui/src/tests/folder.rs b/crates/ui-egui/src/tests/folder.rs new file mode 100644 index 0000000000..e8b4dfae8d --- /dev/null +++ b/crates/ui-egui/src/tests/folder.rs @@ -0,0 +1,76 @@ +//! A test's temporary folder, removed with everything in it when the test ends. + +use std::path::PathBuf; + +/// A test's folder, removed with everything in it when dropped, also when the test panics, as +/// the engine's find-files tests remove their trees. A removal that fails (a file still held open) +/// is ignored. +pub(crate) struct Folder(PathBuf); + +impl Folder { + /// A new empty folder `vectorcraft--` in the system temporary folder. One + /// left there by an earlier process with the same id is removed first. + pub(crate) fn new(name: &str) -> Self { + let dir = std::env::temp_dir().join(format!("vectorcraft-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + Self(dir) + } + + /// The folder at `dir`, made by the caller, removed when dropped. + pub(crate) fn adopt(dir: PathBuf) -> Self { + Self(dir) + } + + /// `bytes` written to a file `name` in the folder → its path. + pub(crate) fn file(&self, name: &str, bytes: &[u8]) -> String { + let path = self.0.join(name); + std::fs::write(&path, bytes).unwrap(); + path.to_string_lossy().into_owned() + } +} + +impl std::ops::Deref for Folder { + type Target = PathBuf; + fn deref(&self) -> &PathBuf { + &self.0 + } +} + +impl Drop for Folder { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +#[test] +fn a_folder_is_removed_when_its_test_ends() { + let folder = Folder::new("ui-folder-ends"); + let path = folder.to_path_buf(); + std::fs::create_dir_all(path.join("nested")).unwrap(); + folder.file("nested/a.txt", b"a"); + assert!(path.join("nested").join("a.txt").is_file()); + drop(folder); + assert!(!path.exists(), "{} is left", path.display()); +} + +#[test] +fn a_folder_is_removed_when_its_test_panics() { + let path = std::env::temp_dir().join(format!("vectorcraft-ui-folder-panics-{}", std::process::id())); + let r = vectorcraft_testkit::catch_quiet::<()>(|| { + let folder = Folder::new("ui-folder-panics"); + folder.file("a.txt", b"a"); + panic!("the test fails"); + }); + assert_eq!(r.err().as_deref(), Some("the test fails")); + assert!(!path.exists(), "{} is left", path.display()); +} + +#[test] +fn an_adopted_test_kit_folder_is_removed_when_its_test_ends() { + let folder = Folder::adopt(vectorcraft_testkit::temp_dir("ui-folder-adopted")); + let path = folder.to_path_buf(); + folder.file("a.txt", b"a"); + drop(folder); + assert!(!path.exists(), "{} is left", path.display()); +} diff --git a/crates/ui-egui/src/tests/place.rs b/crates/ui-egui/src/tests/place.rs index 60f80f4fa4..9c67861eeb 100644 --- a/crates/ui-egui/src/tests/place.rs +++ b/crates/ui-egui/src/tests/place.rs @@ -11,6 +11,7 @@ use vectorcraft_engine::Session; use crate::VectorcraftApp; use crate::canvas::Xf; use crate::place::{DropAt, DropTarget}; +use crate::tests_folder::Folder; /// A `w`×`h` red PNG declaring `ppi`. fn png(w: u32, h: u32, ppi: f64) -> Vec { @@ -19,15 +20,6 @@ fn png(w: u32, h: u32, ppi: f64) -> Vec { vectorcraft_engine::cmd::fileio::ppi::with_png_resolution(&out, (ppi, ppi)) } -/// `bytes` written to a fresh temporary file named `name` → its path. -fn temp_file(name: &str, bytes: &[u8]) -> String { - let dir = std::env::temp_dir().join(format!("vectorcraft-ui-place-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join(name); - std::fs::write(&path, bytes).unwrap(); - path.to_string_lossy().to_string() -} - /// An app reading files from disk, with a 400×300 document. fn app() -> VectorcraftApp { let services = crate::Services { read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))), ..Default::default() }; @@ -166,7 +158,8 @@ fn a_dropped_swatch_library_opens_in_the_library_panel() { let mut app = app(); let ctx = egui::Context::default(); laid_out(&mut app, &ctx); - let gpl = temp_file("dropped.gpl", b"GIMP Palette\nName: Dropped\n#\n255 0 0\tRed\n 0 128 255\tSky\n"); + let dir = Folder::new("ui-place-swatches"); + let gpl = dir.file("dropped.gpl", b"GIMP Palette\nName: Dropped\n#\n255 0 0\tRed\n 0 128 255\tSky\n"); frame(&mut app, &ctx, vec![], &[&gpl], false); let id = format!("loaded/{gpl}"); assert_eq!(crate::panels::library_panel::last_open(&app.ui).map(|o| o.id.as_str()), Some(id.as_str()), "{}", app.ui.status); @@ -184,7 +177,8 @@ fn a_dropped_script_shows_its_check() { let mut app = app(); let ctx = egui::Context::default(); laid_out(&mut app, &ctx); - let path = temp_file("dropped.jsx", b"#target acme\napp.executeMenuCommand('acmecopyall');\n"); + let dir = Folder::new("ui-place-script"); + let path = dir.file("dropped.jsx", b"#target acme\napp.executeMenuCommand('acmecopyall');\n"); frame(&mut app, &ctx, vec![], &[&path], false); let d = app.ui.dialog.as_ref().expect("the check is shown"); assert_eq!(d.kind, crate::dialogs::script_check::KIND, "{}", app.ui.status); @@ -203,7 +197,8 @@ fn a_dropped_libraries_panel_file_is_added_as_a_new_library() { let ctx = egui::Context::default(); laid_out(&mut app, &ctx); let file = json!({"name": "Dropped", "colors": [{"name": "Red", "color": {"model": "rgb", "r": 1, "g": 0, "b": 0}}]}); - let path = temp_file("dropped.vclibrary", file.to_string().as_bytes()); + let dir = Folder::new("ui-place-library"); + let path = dir.file("dropped.vclibrary", file.to_string().as_bytes()); frame(&mut app, &ctx, vec![], &[&path], false); assert_eq!(app.ui.status, "Imported library Dropped"); let lib = app.session.execute("library.get", &json!({})).unwrap(); @@ -220,7 +215,8 @@ fn a_dropped_shortcut_set_opens_import_shortcut_set() { let ctx = egui::Context::default(); let rect = laid_out(&mut app, &ctx); let set = "/Menus {\n /Acme\\ Undo { /Modifiers 11 /Key 902 }\n}\n"; - let path = temp_file("dropped.kys", set.as_bytes()); + let dir = Folder::new("ui-place-shortcuts"); + let path = dir.file("dropped.kys", set.as_bytes()); frame(&mut app, &ctx, vec![], &[&path], false); let kind = |app: &VectorcraftApp| app.ui.dialog.as_ref().map(|d| d.kind.clone()); assert_eq!(kind(&app).as_deref(), Some(crate::dialogs::shortcut_import::KIND), "{}", app.ui.status); @@ -237,7 +233,8 @@ fn a_dropped_png_is_placed_in_the_middle_of_the_view() { let mut app = app(); let ctx = egui::Context::default(); laid_out(&mut app, &ctx); - let path = temp_file("drop.png", &png(20, 10, 72.0)); + let dir = Folder::new("ui-place-png"); + let path = dir.file("drop.png", &png(20, 10, 72.0)); // Where egui last saw the pointer is stale during a desktop drag: it doesn't count. frame(&mut app, &ctx, vec![egui::Event::PointerMoved(Pos2::new(5.0, 5.0))], &[&path], false); let n = selected_image(&app); @@ -258,8 +255,9 @@ fn a_dropped_document_opens_as_a_tab_of_its_own() { let mut app = app(); let ctx = egui::Context::default(); laid_out(&mut app, &ctx); - let svg = temp_file("dropped.svg", SVG.as_bytes()); - let pic = temp_file("with-it.png", &png(8, 8, 72.0)); + let dir = Folder::new("ui-place-tab"); + let svg = dir.file("dropped.svg", SVG.as_bytes()); + let pic = dir.file("with-it.png", &png(8, 8, 72.0)); frame(&mut app, &ctx, vec![], &[&svg, &pic], false); assert_eq!(app.session.documents().len(), 2, "opened, not placed"); assert_eq!(app.session.active_index(), Some(1), "the document opened is shown"); @@ -270,7 +268,8 @@ fn a_dropped_document_opens_as_a_tab_of_its_own() { #[test] fn a_native_svg_drop_uses_file_open_even_with_a_path_only_handle() { - let path = temp_file("path-only.svg", SVG.as_bytes()); + let dir = Folder::new("ui-place-path-only"); + let path = dir.file("path-only.svg", SVG.as_bytes()); let mut dropped = app(); let ctx = egui::Context::default(); laid_out(&mut dropped, &ctx); @@ -317,8 +316,9 @@ fn multiple_svg_drops_open_individually() { let mut app = app(); let ctx = egui::Context::default(); laid_out(&mut app, &ctx); - let a = temp_file("first.svg", SVG.as_bytes()); - let b = temp_file("second.svg", SVG.as_bytes()); + let dir = Folder::new("ui-place-several-svg"); + let a = dir.file("first.svg", SVG.as_bytes()); + let b = dir.file("second.svg", SVG.as_bytes()); frame(&mut app, &ctx, vec![], &[&a, &b], false); assert_eq!(app.session.documents().len(), 3); assert_eq!(app.session.active().unwrap().path.as_deref(), Some(b.as_str())); @@ -330,15 +330,16 @@ fn unsupported_and_unreadable_drops_leave_the_document_open() { let mut app = app(); let ctx = egui::Context::default(); laid_out(&mut app, &ctx); - let invalid = temp_file("not-art.xyz", b"not an image or document"); + let dir = Folder::new("ui-place-unreadable"); + let invalid = dir.file("not-art.xyz", b"not an image or document"); frame(&mut app, &ctx, vec![], &[&invalid], false); assert!(app.ui.status.contains("Couldn't place not-art.xyz"), "{}", app.ui.status); - let invalid_svg = temp_file("broken.svg", b" = ["held-a.png", "held-b.png", "held-c.png"].iter().map(|name| temp_file(name, &png(10, 10, 72.0))).collect(); + let dir = Folder::new("ui-place-held-escape"); + let paths: Vec = ["held-a.png", "held-b.png", "held-c.png"].iter().map(|name| dir.file(name, &png(10, 10, 72.0))).collect(); app.run("file.place", json!({"paths": paths})).unwrap(); crate::dialogs::confirm(&mut app).unwrap(); assert_eq!(app.session.tool_options()["count"], 3); @@ -774,7 +785,8 @@ fn holding_a_chord_bound_to_a_command_repeats_it_with_the_place_cursor_loaded() let ctx = egui::Context::default(); crate::theme::install_fonts(&ctx); app.run("shortcuts.set", json!({"id": "view.zoomIn", "shortcut": "Shift+Escape", "force": true})).unwrap(); - let paths: Vec = ["chord-a.png", "chord-b.png"].iter().map(|name| temp_file(name, &png(10, 10, 72.0))).collect(); + let dir = Folder::new("ui-place-held-chord"); + let paths: Vec = ["chord-a.png", "chord-b.png"].iter().map(|name| dir.file(name, &png(10, 10, 72.0))).collect(); app.run("file.place", json!({"paths": paths})).unwrap(); crate::dialogs::confirm(&mut app).unwrap(); let modifiers = egui::Modifiers::SHIFT; @@ -793,7 +805,8 @@ fn holding_a_chord_bound_to_a_command_repeats_it_with_the_place_cursor_loaded() fn crop_image_shows_a_box_that_apply_crops_to() { use vectorcraft_tools::ToolKey; let mut app = app(); - place_picked(&mut app, &temp_file("crop.png", &png(100, 50, 72.0))); + let dir = Folder::new("ui-place-crop"); + place_picked(&mut app, &dir.file("crop.png", &png(100, 50, 72.0))); app.ui.dialog.as_mut().unwrap().fields.insert("link".into(), json!(false)); crate::dialogs::confirm(&mut app).unwrap(); let whole = selected_image(&app).geometric_bounds().unwrap(); @@ -899,7 +912,8 @@ fn a_file_placed_while_idle_follows_its_changes() { // Half a second later, before the next look is due, a file is placed. The place itself is a // frame (as any command); then nothing happens for 3 s. time += 0.5; - let path = temp_file("idle-watch.png", &png(40, 30, 72.0)); + let dir = Folder::new("ui-place-idle-watch"); + let path = dir.file("idle-watch.png", &png(40, 30, 72.0)); app.run("file.place", json!({"path": path})).unwrap(); let id = app.session.active().unwrap().selection.objects[0]; idle(&mut app, &ctx, &mut time, 3.0, &|_| false); @@ -925,7 +939,8 @@ fn a_look_whose_worker_stopped_does_not_stop_later_looks() { // Every frame renders on this thread: a frame finished by the render worker asks for a repaint. app.canvas.worker_started = true; app.run("prefs.set", json!({"key": "updateLinks", "value": "automatically"})).unwrap(); - let path = temp_file("stopped-watch.png", &png(40, 30, 72.0)); + let dir = Folder::new("ui-place-stopped-watch"); + let path = dir.file("stopped-watch.png", &png(40, 30, 72.0)); app.run("file.place", json!({"path": path})).unwrap(); let id = app.session.active().unwrap().selection.objects[0]; let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/tests/taskbar.rs b/crates/ui-egui/src/tests/taskbar.rs index 4cece18050..d5199e0220 100644 --- a/crates/ui-egui/src/tests/taskbar.rs +++ b/crates/ui-egui/src/tests/taskbar.rs @@ -121,12 +121,10 @@ fn an_image_with_vector_art_offers_make_clipping_mask() { image::RgbaImage::from_pixel(20, 20, image::Rgba([0, 128, 255, 255])) .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) .unwrap(); - let dir = std::env::temp_dir().join(format!("vectorcraft-ui-taskbar-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("blue.png"); - std::fs::write(&path, &png).unwrap(); + let dir = crate::tests_folder::Folder::new("ui-taskbar"); + let path = dir.file("blue.png", &png); app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))); - app.run("file.place", json!({"path": path.to_string_lossy(), "link": false})).unwrap(); + app.run("file.place", json!({"path": path, "link": false})).unwrap(); let img = app.session.active().unwrap().selection.objects[0].0; assert!(matches!(kind(&app, img), NodeKind::Image(_))); let shape = id(app.run("shape.ellipse", json!({"x": 150, "y": 100, "width": 60, "height": 60})).unwrap());