Tests: engine tests remove the temp folders they make

vectorcraft_testkit::temp_dir returns a bare path, so every engine test
that wrote into one left the folder in the system temporary folder.

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