mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 18:15:49 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user