Configurations: reject invalid parameter expressions when saving rows (#9088) (#9414)

* claim: #9088

* Configurations: reject invalid row expressions (#9088)
This commit is contained in:
Colin Faulkingham
2026-10-30 13:32:50 +00:00
committed by GitHub
parent c0b0369aa2
commit 8b25fa0ae4
+63 -3
View File
@@ -294,6 +294,22 @@ fn row(s: &mut Session, p: &Value) -> Result<Value> {
if rename.as_ref().is_some_and(|n| n != &name && s.doc.configs.rows.iter().any(|r| &r.name == n)) {
return Err(bad(cmd, "configuration name already exists"));
}
let changes: Vec<_> = s
.doc
.configs
.columns
.iter()
.zip(&cells)
.filter_map(|(column, value)| match column {
Column::Param(name) => Some((name.clone(), value.clone())),
_ => None,
})
.collect();
if !changes.is_empty() {
// Check the final parameter graph without activating the row or rebuilding geometry.
let mut trial = s.doc.as_ref().clone();
trial.change_params_atomic(&changes).map_err(|e| bad(cmd, e.to_string()))?;
}
let t = &mut s.doc_mut().configs;
let validated_name = rename.clone().unwrap_or_else(|| name.clone());
match idx {
@@ -664,6 +680,52 @@ mod tests {
assert!((volume(&mut s) - 2000.0).abs() < 1e-6);
}
#[test]
fn configuration_rows_reject_invalid_parameter_expressions_without_changes() {
let mut s = Session::default();
run(&mut s, "parameters.add", json!({"name":"L", "expression":"10 mm"}));
run(&mut s, "config.configure", json!({"params":["L"]}));
let before = s.doc.clone();
let undo_len = s.undo.len();
for expression in ["abc", "1/", "1/0"] {
for params in [
json!({"name":"Bad", "values":{"param:L":expression}}),
json!({"name":"Copy", "from":"Default", "values":{"L":expression}}),
json!({"name":"Default", "rename":"Renamed", "values":{"L":expression}}),
] {
let error = s.execute("config.row", &params).expect_err("invalid expressions must be rejected when saving a row");
assert!(error.to_string().contains("parameter `L`"), "{params}: {error}");
assert_eq!(s.doc, before, "{params}");
assert_eq!(s.undo.len(), undo_len, "{params}");
}
}
}
#[test]
fn configuration_rows_validate_the_final_parameter_graph_without_activating_it() {
let mut s = Session::default();
run(&mut s, "parameters.add", json!({"name":"L", "expression":"10 mm"}));
run(&mut s, "parameters.add", json!({"name":"W", "expression":"L + 2 mm"}));
run(&mut s, "config.configure", json!({"params":["L", "W"]}));
let before = s.doc.clone();
// Changing L first would make a cycle with W's current expression. The final row is valid.
run(&mut s, "config.row", json!({"name":"Reordered", "values":{"L":"W + 2 mm", "W":"20 mm"}}));
assert_eq!(s.doc.params, before.params);
assert_eq!(s.doc.configs.active, before.configs.active);
let saved = s.doc.clone();
let undo_len = s.undo.len();
assert!(s.execute("config.row", &json!({"name":"Cycle", "from":"Reordered", "values":{"W":"L"}})).is_err());
assert_eq!(s.doc, saved);
assert_eq!(s.undo.len(), undo_len);
run(&mut s, "config.activate", json!({"row":"Reordered"}));
let (values, errors) = s.doc.param_values();
assert!(errors.is_empty(), "{errors:?}");
assert_eq!(values["L"].v, 22.0);
assert_eq!(values["W"].v, 20.0);
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc, saved);
}
#[test]
fn configured_feature_inputs_and_suppression_match_typed_cells() {
let mut s = Session::default();
@@ -746,9 +808,7 @@ mod tests {
// Bad input.
assert!(s.execute("config.row", &json!({"name": "X", "values": {"nope": 1}})).is_err());
assert!(s.execute("config.activate", &json!({"row": "Missing"})).is_err());
run(&mut s, "config.row", json!({"name": "Broken", "values": {"length": "1/"}}));
assert!(s.execute("config.activate", &json!({"row": "Broken"})).is_err());
run(&mut s, "config.row", json!({"name": "Broken", "delete": true}));
assert!(s.execute("config.row", &json!({"name": "Broken", "values": {"length": "1/"}})).is_err());
run(&mut s, "config.column", json!({"feature": fillet, "delete": true}));
assert_eq!(run(&mut s, "config.table", json!({}))["columns"].as_array().map(Vec::len), Some(1));
}