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.
This commit is contained in:
dotCooCoo
2026-11-13 07:14:08 -08:00
parent e8c7421a3d
commit 07522fb4e6
9 changed files with 171 additions and 106 deletions
+12 -23
View File
@@ -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(&params, &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);
@@ -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#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><text x="10" y="40" font-family="sans-serif">A</text></svg>"#;
crate::io::open_bytes(&mut app, "generic.svg", svg.as_bytes(), None).unwrap();
let listed = app.session.execute("text.fonts", &json!({})).unwrap();
@@ -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<RefCell<Vec<(String, Vec<u8>)>>>;
@@ -197,7 +198,7 @@ fn board_files_of(app: &VectorcraftApp, boards: [bool; 4]) -> Vec<Option<String>
#[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}");
+2 -2
View File
@@ -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;
+8 -25
View File
@@ -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
+1
View File
@@ -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;
+76
View File
@@ -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-<name>-<process id>` 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());
}
+54 -39
View File
@@ -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<u8> {
@@ -19,15 +20,6 @@ fn png(w: u32, h: u32, ppi: f64) -> Vec<u8> {
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"<svg");
let invalid_svg = dir.file("broken.svg", b"<svg");
frame(&mut app, &ctx, vec![], &[&invalid_svg], false);
assert!(app.ui.status.contains("Couldn't open broken.svg"), "{}", app.ui.status);
let missing = temp_file("unreadable.svg", SVG.as_bytes());
let missing = dir.file("unreadable.svg", SVG.as_bytes());
std::fs::remove_file(&missing).unwrap();
frame(&mut app, &ctx, vec![], &[&missing], false);
assert!(app.ui.status.contains("Couldn't open unreadable.svg"), "{}", app.ui.status);
@@ -350,7 +351,8 @@ fn unsupported_and_unreadable_drops_leave_the_document_open() {
fn files_dropped_with_no_document_open_open_and_are_recent() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
let ctx = egui::Context::default();
let path = temp_file("open-me.png", &png(8, 8, 72.0));
let dir = Folder::new("ui-place-no-document");
let path = dir.file("open-me.png", &png(8, 8, 72.0));
frame(&mut app, &ctx, vec![], &[], false);
frame(&mut app, &ctx, vec![], &[&path], false);
assert_eq!(app.session.documents().len(), 1, "no document: the drop opens");
@@ -407,8 +409,9 @@ fn a_file_read_after_another_document_became_active_lands_where_it_was_dropped()
#[test]
fn the_place_dialog_lists_the_files_and_loads_the_cursor_with_several() {
let mut app = app();
let a = temp_file("a.png", &png(300, 150, 300.0));
let b = temp_file("b.png", &png(10, 10, 72.0));
let dir = Folder::new("ui-place-dialog");
let a = dir.file("a.png", &png(300, 150, 300.0));
let b = dir.file("b.png", &png(10, 10, 72.0));
app.run("file.place", json!({"paths": [a, b]})).unwrap();
let d = app.ui.dialog.as_ref().expect("the Place dialog");
assert_eq!(d.kind, crate::dialogs::place::KIND);
@@ -448,7 +451,8 @@ fn the_place_dialog_lists_the_files_and_loads_the_cursor_with_several() {
fn one_file_places_centred_in_the_view_and_replace_swaps_the_selection() {
let mut app = app();
app.view_mut().unwrap().center = vectorcraft_geom::Point::new(120.0, 80.0);
place_picked(&mut app, &temp_file("one.png", &png(300, 150, 300.0)));
let dir = Folder::new("ui-place-replace");
place_picked(&mut app, &dir.file("one.png", &png(300, 150, 300.0)));
let d = app.ui.dialog.as_ref().expect("the Place dialog");
assert_eq!(d.kind, crate::dialogs::place::KIND);
assert!(d.bool("link") && !d.bool("__replace"), "Link on; Replace needs one selected object");
@@ -462,7 +466,7 @@ fn one_file_places_centred_in_the_view_and_replace_swaps_the_selection() {
let first = selected_image(&app);
assert_eq!(first.geometric_bounds().unwrap().center(), vectorcraft_geom::Point::new(120.0, 80.0), "centred in the view");
// With one object selected, Replace applies.
place_picked(&mut app, &temp_file("two.png", &png(40, 40, 144.0)));
place_picked(&mut app, &dir.file("two.png", &png(40, 40, 144.0)));
assert!(app.ui.dialog.as_ref().unwrap().bool("__replace"));
app.ui.dialog.as_mut().unwrap().fields.insert("replace".into(), json!(true));
app.ui.dialog.as_mut().unwrap().fields.insert("link".into(), json!(false));
@@ -476,7 +480,8 @@ fn one_file_places_centred_in_the_view_and_replace_swaps_the_selection() {
#[test]
fn the_control_bar_shows_the_image_file_link_colour_mode_and_ppi() {
let mut app = app();
let path = temp_file("photo.png", &png(30, 30, 300.0));
let dir = Folder::new("ui-place-control-bar");
let path = dir.file("photo.png", &png(30, 30, 300.0));
app.run("file.place", json!({"path": path})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, crate::chrome::control_bar);
for s in ["Linked File", "photo.png", "RGB PPI: 300"] {
@@ -493,7 +498,8 @@ fn the_control_bar_shows_the_image_file_link_colour_mode_and_ppi() {
#[test]
fn the_control_bar_label_follows_edits_undo_documents_and_selection() {
let mut app = app();
let path = temp_file("label.png", &png(30, 30, 72.0));
let dir = Folder::new("ui-place-label");
let path = dir.file("label.png", &png(30, 30, 72.0));
app.run("file.place", json!({"path": path})).unwrap();
let text = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, crate::chrome::control_bar);
assert!(text(&mut app).contains("Linked File"));
@@ -539,7 +545,8 @@ fn the_control_bars_file_name_relinks_the_image() {
for s in ["Replace…", "Go To Link", "Edit Original", "Update Link", "Placement Options…"] {
item(&mut app, &ctx, s);
}
let other = temp_file("blue.png", &png(60, 30, 72.0));
let dir = Folder::new("ui-place-relink");
let other = dir.file("blue.png", &png(60, 30, 72.0));
let picked = other.clone();
app.services.pick_open = Some(Box::new(move |_: &crate::FilePick| Some(picked.clone())));
let p = item(&mut app, &ctx, "Replace…");
@@ -657,7 +664,8 @@ fn a_vectorcraft_document_places_linked_and_edit_original_opens_it() {
src.execute("file.new", &json!({"width": 100, "height": 50})).unwrap();
src.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 50})).unwrap();
let bytes = vectorcraft_format::save_file(&src.active().unwrap().doc);
let path = temp_file("badge.vectorcraft", &bytes);
let dir = Folder::new("ui-place-document");
let path = dir.file("badge.vectorcraft", &bytes);
place_picked(&mut app, &path);
let d = app.ui.dialog.as_ref().expect("the Place dialog");
assert!(d.bool("link") && d.bool("__documents") && !d.bool("__others"), "Link on, for a document");
@@ -674,7 +682,7 @@ fn a_vectorcraft_document_places_linked_and_edit_original_opens_it() {
assert_eq!(app.session.documents().len(), tabs + 1);
assert_eq!(app.session.active().unwrap().path.as_deref(), Some(path.as_str()));
// A PNG keeps the image tooltip; a PNG and a document, both.
let pic = temp_file("pic.png", &png(10, 10, 72.0));
let pic = dir.file("pic.png", &png(10, 10, 72.0));
place_picked(&mut app, &pic);
let d = app.ui.dialog.as_ref().unwrap();
assert!(!d.bool("__documents") && crate::dialogs::place::link_tip(d).contains("image file"));
@@ -693,7 +701,8 @@ fn the_place_dialog_links_a_pdf_page_and_embeds_it_with_link_off() {
src.execute("file.new", &json!({"width": 100, "height": 50})).unwrap();
src.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 50})).unwrap();
let pdf = src.execute("document.serialize", &json!({"format": "pdf", "preserveEditing": false})).unwrap();
let path = temp_file("flyer.pdf", &vectorcraft_format::base64_decode(pdf["dataBase64"].as_str().unwrap()).unwrap());
let dir = Folder::new("ui-place-pdf");
let path = dir.file("flyer.pdf", &vectorcraft_format::base64_decode(pdf["dataBase64"].as_str().unwrap()).unwrap());
// Link on (the default): one locked object, linked to the file.
place_picked(&mut app, &path);
let d = app.ui.dialog.as_ref().expect("the Place dialog");
@@ -715,7 +724,8 @@ fn the_place_dialog_links_a_pdf_page_and_embeds_it_with_link_off() {
fn the_place_dialog_does_not_offer_a_postscript_ai_as_a_locked_object() {
let mut app = app();
let ps = b"%!PS-Adobe-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n1 0 0 setrgbcolor 10 10 50 50 rectfill\nshowpage\n%%EOF\n";
let path = temp_file("legacy.ai", ps);
let dir = Folder::new("ui-place-postscript");
let path = dir.file("legacy.ai", ps);
// file.place embeds a .ai saved without PDF compatibility, even with Link on.
let r = app.session.execute("file.place", &json!({"path": path, "link": true})).unwrap();
assert_eq!(r["linked"], json!(false), "{r}");
@@ -748,7 +758,8 @@ fn holding_escape_with_the_place_cursor_loaded_discards_one_file() {
app.run("shortcuts.set", json!({"id": "select.none", "shortcut": "Escape", "force": true})).unwrap();
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [id]})).unwrap();
let paths: Vec<String> = ["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<String> = ["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<String> = ["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<String> = ["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();
+3 -5
View File
@@ -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());