Merge pull request #7166 from dotCooCoo/fix/test-temp-folder-cleanup

Tests: ui-egui tests remove the temp folders they make
This commit is contained in:
BFlat
2026-11-13 23:15:19 +07:00
committed by GitHub
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());