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());