Tests: the command sweep sends file and folder params only into its temp folder

The tool-edit sweep sent `{"<key>": "on"}` for every documented key, so
`prefs.export` wrote a preferences file named `on` into crates/engine on
every run, and `folder`, `destinationFolder` and `diffPath` strings from
`junk_params` named locations under the working directory.

- `is_path_key` covers `path`, `diffPath`, `folder` and
  `destinationFolder`; `safe_value` sends a temp file for a file key and
  a temp folder for a folder key, and both sweeps use it.
- A new test builds both sweeps' cases for every command and fails when
  a string under one of those keys is outside the temp folder, without
  running any command.
This commit is contained in:
dotCooCoo
2026-11-11 02:38:07 -08:00
parent e1c199a184
commit 2b378689d1
2 changed files with 73 additions and 20 deletions
+52 -15
View File
@@ -9,10 +9,11 @@ use vectorcraft_engine::{Session, command_specs};
use vectorcraft_testkit::catch_quiet;
use vectorcraft_testkit::fixtures::Fixture;
use vectorcraft_testkit::invariants::{check_all, check_session, doc_json};
use vectorcraft_testkit::strategies::{junk_params, junk_values, param_keys};
use vectorcraft_testkit::strategies::{junk_params, junk_values, param_keys, safe_value};
/// Commands that replace/close the active document or touch the filesystem are fine to call, but a
/// few need their paths redirected into a temp dir (handled by `junk_params`).
/// few need their file and folder params redirected into a temp dir (`safe_value`, which
/// `junk_params` applies).
fn safe_path() -> String {
vectorcraft_testkit::temp_dir("sweep").join("out.bin").to_string_lossy().to_string()
}
@@ -230,6 +231,54 @@ fn tool_edit_state(s: &Session) -> ToolEditState {
(docs, s.active_index(), s.paint.clone())
}
/// The params the tool-edit sweep sends `c`: junk params, then each documented key turned on and
/// off, with a string under a file or folder key replaced by a location inside `path`'s folder.
fn tool_edit_cases(c: &vectorcraft_engine::CommandSpec, path: &str) -> Vec<Value> {
let mut cases = junk_params(c.params, path);
if heavy(c.id) {
cases.truncate(12);
}
for k in param_keys(c.params) {
for v in [json!(true), json!(false), json!(1), json!("on")] {
cases.push(json!({ k.as_str(): safe_value(&k, &v, path) }));
}
}
// Find Objects selects only with a filter, which one key at a time does not give it.
if c.id == "document.find" {
cases.extend([json!({"kind": "Path", "select": true}), json!({"name": "no such name", "select": true})]);
}
cases
}
/// Every string the sweeps send under a key that names a file or folder to write is inside the
/// sweep's temp folder, so no run writes into the working directory.
#[test]
fn sweep_cases_name_files_only_inside_the_temp_folder() {
let path = safe_path();
let dir = vectorcraft_testkit::temp_dir("sweep");
let keys = ["path", "diffPath", "folder", "destinationFolder"];
let mut seen = std::collections::BTreeSet::new();
let mut outside = std::collections::BTreeSet::new();
for c in command_specs() {
let mut cases = junk_params(c.params, &path);
cases.extend(tool_edit_cases(c, &path));
for p in &cases {
let Some(o) = p.as_object() else { continue };
for (k, v) in o.iter().filter(|(k, _)| keys.contains(&k.as_str())) {
seen.insert(k.clone());
if let Some(s) = v.as_str()
&& !std::path::Path::new(s).starts_with(&dir)
{
outside.insert(format!("{} {{\"{k}\": {v}}}", c.id));
}
}
}
}
assert_eq!(seen.len(), keys.len(), "the sweeps send only {seen:?}");
let outside: Vec<String> = outside.into_iter().collect();
assert!(outside.is_empty(), "{} cases name a location outside {}:\n{}", outside.len(), dir.display(), outside.join("\n"));
}
/// Every command that runs at once while the active tool computes an edit on another thread (a
/// Shape Builder merge), rather than applying the edit first (`waits_for_tool_edit`), leaves the
/// documents' revisions and selections, the active document and the current paint as they were,
@@ -241,19 +290,7 @@ fn commands_that_do_not_wait_for_a_tool_edit_change_no_document() {
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)) {
let mut cases = junk_params(c.params, &path);
if heavy(c.id) {
cases.truncate(12);
}
for k in param_keys(c.params) {
for v in [json!(true), json!(false), json!(1), json!("on")] {
cases.push(json!({ k.as_str(): v }));
}
}
// Find Objects selects only with a filter, which one key at a time does not give it.
if c.id == "document.find" {
cases.extend([json!({"kind": "Path", "select": true}), json!({"name": "no such name", "select": true})]);
}
let cases = tool_edit_cases(c, &path);
for p in &cases {
let mut s = fx.session();
let before = tool_edit_state(&s);
+21 -5
View File
@@ -502,10 +502,26 @@ pub fn param_keys(doc: &str) -> Vec<String> {
out
}
/// Parameter-name keys that name filesystem paths: fuzzing these with arbitrary strings could
/// write files anywhere, so callers substitute a temp path.
/// Parameter-name keys that name a file or folder a command may write: fuzzing these with
/// arbitrary strings could write files anywhere, so callers substitute a temp path.
pub fn is_path_key(k: &str) -> bool {
k == "path"
matches!(k, "path" | "diffPath") || is_folder_key(k)
}
/// Path keys that name a folder a command creates or writes files into.
pub fn is_folder_key(k: &str) -> bool {
matches!(k, "folder" | "destinationFolder")
}
/// The location to send under the path key `k`: `safe_path` for a file, a folder next to it for
/// a folder key.
pub fn safe_location(k: &str, safe_path: &str) -> String {
if is_folder_key(k) { format!("{safe_path}-folder") } else { safe_path.to_string() }
}
/// `v`, or the temp location for `k` when `k` is a path key and `v` is a string.
pub fn safe_value(k: &str, v: &Value, safe_path: &str) -> Value {
if is_path_key(k) && v.is_string() { json!(safe_location(k, safe_path)) } else { v.clone() }
}
/// A deterministic list of junk param objects for `doc` (the command's params doc): each
@@ -516,7 +532,7 @@ pub fn junk_params(doc: &str, safe_path: &str) -> Vec<Value> {
let mut out = vec![Value::Null, json!([]), json!("str"), json!(42), json!({"unknownKey": 1})];
for k in &keys {
for (i, v) in junk.iter().enumerate() {
let v = if is_path_key(k) && v.is_string() { json!(safe_path) } else { v.clone() };
let v = safe_value(k, v, safe_path);
// Sparse: one key at a time with every junk value, plus a few combos.
let mut o = serde_json::Map::new();
o.insert(k.clone(), v.clone());
@@ -524,7 +540,7 @@ pub fn junk_params(doc: &str, safe_path: &str) -> Vec<Value> {
if i % 5 == 0 {
let mut all = serde_json::Map::new();
for k2 in &keys {
let v2 = if is_path_key(k2) { json!(safe_path) } else { v.clone() };
let v2 = if is_path_key(k2) { json!(safe_location(k2, safe_path)) } else { v.clone() };
all.insert(k2.clone(), v2);
}
out.push(Value::Object(all));