app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
//! THE CSWA BENCHMARK (0.11.0) — exam parts as release validation.
//!
//! Drew (2026-10-15): "pull 3-5 random parts from CSWA exams as part of our
//! e2e testing/validation for each release … use those as benchmarks for new
//! feature developments." Each spec in `resources/cswa/` (shipped in the zip)
//! is one part: the published answer (mass in grams, sometimes volume, centre
//! of mass, surface area), the material, and the `swx_run_script` that builds
//! it. `swx_selftest {level:"cswa"}` and `tools/cswa-check.py` run them and
//! compare within the exam's tolerance; `tools/release.sh --require-live`
//! refuses to publish without a passing live report.
//!
//! The answer key is the PUBLISHED one, never the bridge's own idea: a spec
//! whose key was not published says where its number came from (`keyBasis`).
use serde_json::{json, Value};
/// Every shipped spec, embedded so the self-test needs no file next to the
/// exe (the same files ship in the zip for tools/cswa-check.py). A test pins
/// this list to the directory.
pub const SPECS: &[(&str, &str)] = &[
("cswa-00-lego-2x4.json", include_str!("../resources/cswa/cswa-00-lego-2x4.json")),
("cswa-01-3dengr-q5-block.json", include_str!("../resources/cswa/cswa-01-3dengr-q5-block.json")),
("cswa-02-3dengr-q6-hole.json", include_str!("../resources/cswa/cswa-02-3dengr-q6-hole.json")),
("cswa-03-3dengr-q8-hole30.json", include_str!("../resources/cswa/cswa-03-3dengr-q8-hole30.json")),
("cswa-04-3dengr-q9-l-bracket.json", include_str!("../resources/cswa/cswa-04-3dengr-q9-l-bracket.json")),
("cswa-05-tool-block-step1.json", include_str!("../resources/cswa/cswa-05-tool-block-step1.json")),
("cswa-06-tool-block-step2.json", include_str!("../resources/cswa/cswa-06-tool-block-step2.json")),
("cswa-07-tool-block-step3.json", include_str!("../resources/cswa/cswa-07-tool-block-step3.json")),
("cswa-08-tool-block-step4.json", include_str!("../resources/cswa/cswa-08-tool-block-step4.json")),
("cswa-08b-tool-block-step4-explicit.json", include_str!("../resources/cswa/cswa-08b-tool-block-step4-explicit.json")),
("cswa-09-community-stepped-shaft.json", include_str!("../resources/cswa/cswa-09-community-stepped-shaft.json")),
("cswa-10-community-mirrored-block.json", include_str!("../resources/cswa/cswa-10-community-mirrored-block.json")),
("cswa-11-community-slotted-plate.json", include_str!("../resources/cswa/cswa-11-community-slotted-plate.json")),
("cswa-12-community-up-to-plates.json", include_str!("../resources/cswa/cswa-12-community-up-to-plates.json")),
];
// ── SPEC VARIABLES (0.11.0) ────────────────────────────────────────────────
// An exam part is often the SAME drawing with new values (the Tool Block's
// Steps 1 and 2: A, B, C change, "all other dimensions the same"). A spec may
// therefore name `variables` (numbers), `derived` ([name, expression] pairs,
// evaluated in order) and write any number in its script as "=expression";
// `extends: "<id>"` takes another spec's script and derived values and
// overrides its variables. This lives in the BENCHMARK (here and in
// tools/cswa-check.py, which implements the same grammar), never in the DSL:
// the bridge is always sent plain numbers.
/// Evaluate an arithmetic expression: numbers, variables, + − * / ^,
/// parentheses, unary minus, `pi`, and sqrt sin cos tan asin acos atan atan2
/// abs min max rad deg (angles in RADIANS; rad()/deg() convert).
pub fn eval(expr: &str, vars: &std::collections::HashMap<String, f64>) -> Result<f64, String> {
struct P<'a> {
s: &'a [u8],
i: usize,
vars: &'a std::collections::HashMap<String, f64>,
}
impl P<'_> {
fn ws(&mut self) {
while self.i < self.s.len() && (self.s[self.i] as char).is_whitespace() {
self.i += 1;
}
}
fn peek(&mut self) -> Option<char> {
self.ws();
self.s.get(self.i).map(|c| *c as char)
}
fn expr(&mut self) -> Result<f64, String> {
let mut v = self.term()?;
while let Some(c) = self.peek() {
match c {
'+' => { self.i += 1; v += self.term()?; }
'-' => { self.i += 1; v -= self.term()?; }
_ => break,
}
}
Ok(v)
}
fn term(&mut self) -> Result<f64, String> {
let mut v = self.power()?;
while let Some(c) = self.peek() {
match c {
'*' => { self.i += 1; v *= self.power()?; }
'/' => { self.i += 1; v /= self.power()?; }
_ => break,
}
}
Ok(v)
}
fn power(&mut self) -> Result<f64, String> {
let base = self.unary()?;
if self.peek() == Some('^') {
self.i += 1;
return Ok(base.powf(self.power()?));
}
Ok(base)
}
fn unary(&mut self) -> Result<f64, String> {
match self.peek() {
Some('-') => { self.i += 1; Ok(-self.unary()?) }
Some('+') => { self.i += 1; self.unary() }
_ => self.atom(),
}
}
fn atom(&mut self) -> Result<f64, String> {
match self.peek() {
Some('(') => {
self.i += 1;
let v = self.expr()?;
if self.peek() != Some(')') {
return Err("missing )".into());
}
self.i += 1;
Ok(v)
}
Some(c) if c.is_ascii_digit() || c == '.' => {
let st = self.i;
while self.i < self.s.len() && ((self.s[self.i] as char).is_ascii_digit() || self.s[self.i] == b'.' || self.s[self.i] == b'e'
|| ((self.s[self.i] == b'-' || self.s[self.i] == b'+') && self.i > st && self.s[self.i - 1] == b'e')) {
self.i += 1;
}
std::str::from_utf8(&self.s[st..self.i]).unwrap_or("").parse::<f64>().map_err(|e| format!("bad number: {e}"))
}
Some(c) if c.is_ascii_alphabetic() || c == '_' => {
let st = self.i;
while self.i < self.s.len() && ((self.s[self.i] as char).is_ascii_alphanumeric() || self.s[self.i] == b'_') {
self.i += 1;
}
let name = std::str::from_utf8(&self.s[st..self.i]).unwrap_or("").to_string();
if self.peek() == Some('(') {
self.i += 1;
let mut args = vec![self.expr()?];
while self.peek() == Some(',') {
self.i += 1;
args.push(self.expr()?);
}
if self.peek() != Some(')') {
return Err(format!("missing ) after {name}("));
}
self.i += 1;
let one = |f: fn(f64) -> f64| if args.len() == 1 { Ok(f(args[0])) } else { Err(format!("{name} takes one argument")) };
let two = |f: fn(f64, f64) -> f64| if args.len() == 2 { Ok(f(args[0], args[1])) } else { Err(format!("{name} takes two arguments")) };
return match name.as_str() {
"sqrt" => one(f64::sqrt),
"sin" => one(f64::sin),
"cos" => one(f64::cos),
"tan" => one(f64::tan),
"asin" => one(f64::asin),
"acos" => one(f64::acos),
"atan" => one(f64::atan),
"abs" => one(f64::abs),
"rad" => one(f64::to_radians),
"deg" => one(f64::to_degrees),
"atan2" => two(f64::atan2),
"min" => two(f64::min),
"max" => two(f64::max),
_ => Err(format!("unknown function {name}")),
};
}
if name == "pi" {
return Ok(std::f64::consts::PI);
}
self.vars.get(&name).copied().ok_or_else(|| format!("unknown variable {name}"))
}
other => Err(format!("unexpected {other:?}")),
}
}
}
let mut p = P { s: expr.as_bytes(), i: 0, vars };
let v = p.expr().map_err(|e| format!("{e} in {expr:?}"))?;
if p.peek().is_some() {
return Err(format!("unexpected text at {} in {expr:?}", p.i));
}
if !v.is_finite() {
return Err(format!("{expr:?} is not a finite number (with these variables)"));
}
Ok(v)
}
/// A spec with its `extends` chain merged in (the base's script, derived values
/// and variables, overridden or appended to by each child in turn).
fn resolve_extends(v: &Value, all: &[Value], depth: usize) -> Result<Value, String> {
let id = v.get("id").and_then(|i| i.as_str()).unwrap_or("?").to_string();
let Some(base_id) = v.get("extends").and_then(|e| e.as_str()) else { return Ok(v.clone()) };
if depth > 8 {
return Err(format!("{id}: extends goes more than 8 deep (a cycle?)"));
}
let base = all.iter().find(|b| b.get("id").and_then(|i| i.as_str()) == Some(base_id)).ok_or_else(|| format!("{id}: extends {base_id:?}, which is not a spec"))?;
let mut merged = resolve_extends(base, all, depth + 1)?;
let mut vars = merged.get("variables").cloned().unwrap_or(json!({}));
if let (Value::Object(m), Some(Value::Object(o))) = (&mut vars, v.get("variables")) {
for (k, x) in o {
m.insert(k.clone(), x.clone());
}
}
if let (Value::Object(m), Value::Object(o)) = (&mut merged, v) {
// What is TRUE OF THE BASE PART ONLY is not inherited: the ops it
// lacked, the derivation of ITS key, why IT is skipped in the simulator.
m.remove("needsOps");
m.remove("derivation");
m.remove("simulatorSkips");
for (k, x) in o {
if k != "extends" && k != "variables" && k != "scriptAppend" && k != "derivedAppend" {
m.insert(k.clone(), x.clone());
}
}
m.insert("variables".into(), vars);
// A derivedAppend name the base already derives would SHADOW it — and
// every "=name" in the base's script would silently take the new value
// (0.11.4: Step 4's tangent point `tpx` redrew Step 1's R5 fillet).
// Refused: an appended value gets a name of its own.
if let (Some(Value::Array(extra)), Some(Value::Array(base))) = (o.get("derivedAppend"), m.get("derived")) {
let taken: Vec<&str> = base.iter().filter_map(|p| p.get(0).and_then(|n| n.as_str())).collect();
if let Some(clash) = extra.iter().filter_map(|p| p.get(0).and_then(|n| n.as_str())).find(|n| taken.contains(n)) {
return Err(format!("{id}: derivedAppend redefines {clash:?}, which {base_id} already derives — name it differently"));
}
}
// More steps after the base part's (the Tool Block's Steps 3 and 4
// cut the Step 2 part), and more derived values for them.
for (key, into) in [("scriptAppend", "script"), ("derivedAppend", "derived")] {
if let Some(extra) = o.get(key) {
let more = extra.as_array().ok_or_else(|| format!("{id}: {key} must be a list"))?;
let list = m.entry(into.to_string()).or_insert_with(|| json!([]));
if let Value::Array(a) = list {
a.extend(more.iter().cloned());
}
}
}
}
Ok(merged)
}
/// Resolve `extends`, `variables` and `derived`, and replace every
/// "=expression" string in the script with its value. A spec without them is
/// returned as it is.
pub fn expand(v: &Value, all: &[Value]) -> Result<Value, String> {
let id = v.get("id").and_then(|i| i.as_str()).unwrap_or("?").to_string();
let mut spec = resolve_extends(v, all, 0)?;
let mut vars = std::collections::HashMap::new();
if let Some(Value::Object(m)) = spec.get("variables") {
for (k, x) in m {
vars.insert(k.clone(), x.as_f64().ok_or_else(|| format!("{id}: variable {k} must be a number"))?);
}
}
if let Some(d) = spec.get("derived") {
for pair in d.as_array().ok_or_else(|| format!("{id}: derived is a list of [name, expression]"))? {
let (name, e) = match pair.as_array().map(|a| a.as_slice()) {
Some([Value::String(n), Value::String(e)]) => (n.clone(), e.clone()),
_ => return Err(format!("{id}: derived is a list of [name, expression]")),
};
let x = eval(&e, &vars).map_err(|m| format!("{id}: derived {name}: {m}"))?;
vars.insert(name, x);
}
}
fn subst(v: &mut Value, vars: &std::collections::HashMap<String, f64>) -> Result<(), String> {
match v {
Value::String(s) if s.starts_with('=') => {
*v = json!(eval(&s[1..], vars)?);
}
Value::Array(a) => {
for x in a {
subst(x, vars)?;
}
}
Value::Object(m) => {
for (_, x) in m.iter_mut() {
subst(x, vars)?;
}
}
_ => {}
}
Ok(())
}
for key in ["script", "expected"] {
if let Some(x) = spec.get_mut(key) {
subst(x, &vars).map_err(|m| format!("{id}: {key}: {m}"))?;
}
}
Ok(spec)
}
/// One benchmark part.
#[derive(Debug, Clone, PartialEq)]
pub struct Spec {
pub id: String,
pub title: String,
pub official: bool,
pub material: Option<String>,
pub density: Option<f64>,
pub mass_g: f64,
pub volume_mm3: Option<f64>,
pub com_mm: Option<[f64; 3]>,
pub area_mm2: Option<f64>,
pub tol_pct: f64,
pub script: Value,
/// Ops the part needs that the bridge does not have yet: the part is
/// SKIPPED (and said so), never half-built.
pub needs_ops: Vec<String>,
/// Why the SIMULATOR cannot build this part (0.11.4) — e.g. Offset
/// Entities needs SOLIDWORKS' own face outline. Skipped there, said so;
/// run live as any other part.
pub simulator_skips: Option<String>,
/// HOW A DERIVED KEY IS REPRODUCED (0.11.4): `[name, Python expression]`
/// pairs evaluated in order (math's functions, the spec's variables and
/// derived values in scope); `massG`, and `volumeMm3` / `centerOfMassMm`
/// where the key has them, must come out as the key says.
/// `tools/cswa-check.py --derivations` checks every one.
pub derivation: Vec<(String, String)>,
}
pub fn parse_spec(v: &Value) -> Result<Spec, String> {
let s = |k: &str| v.get(k).and_then(|x| x.as_str()).map(String::from);
let id = s("id").ok_or("a spec needs an id")?;
if v.get("units").and_then(|u| u.as_str()).unwrap_or("MMGS") != "MMGS" {
return Err(format!("{id}: units must be MMGS (the DSL is millimetres; convert an IPS part's dimensions first)"));
}
let e = v.get("expected").ok_or_else(|| format!("{id}: expected is required"))?;
let f = |k: &str| e.get(k).and_then(|x| x.as_f64()).filter(|x| x.is_finite());
let mass_g = f("massG").filter(|m| *m > 0.0).ok_or_else(|| format!("{id}: expected.massG (grams, > 0) is required"))?;
let tol_pct = f("tolerancePct").unwrap_or(1.0);
if !(tol_pct > 0.0 && tol_pct <= 5.0) {
return Err(format!("{id}: tolerancePct must be in (0, 5]"));
}
let com_mm = match e.get("centerOfMassMm") {
None | Some(Value::Null) => None,
Some(c) => {
let a: Vec<f64> = c.as_array().map(|a| a.iter().filter_map(|x| x.as_f64()).collect()).unwrap_or_default();
if a.len() != 3 {
return Err(format!("{id}: expected.centerOfMassMm must be [x, y, z]"));
}
Some([a[0], a[1], a[2]])
}
};
let script = v.get("script").cloned().ok_or_else(|| format!("{id}: script is required"))?;
let needs_ops: Vec<String> = v.get("needsOps").and_then(|n| n.as_array()).map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect()).unwrap_or_default();
let simulator_skips = match v.get("simulatorSkips") {
None | Some(Value::Null) => None,
Some(Value::String(why)) if !why.trim().is_empty() => Some(why.clone()),
Some(_) => return Err(format!("{id}: simulatorSkips is the reason the simulator cannot build the part (a sentence)")),
};
let derivation = derivation_pairs(v).map_err(|m| format!("{id}: {m}"))?;
if needs_ops.is_empty() {
// A runnable spec must PARSE: a typo is caught here, not on a live run.
crate::script::parse(&script).map_err(|m| format!("{id}: its script does not parse: {m}"))?;
}
Ok(Spec {
title: s("title").unwrap_or_else(|| id.clone()),
official: v.get("official").and_then(|o| o.as_bool()).unwrap_or(false),
material: v.pointer("/material/name").and_then(|m| m.as_str()).map(String::from),
density: v.pointer("/material/densityKgM3").and_then(|m| m.as_f64()),
mass_g,
volume_mm3: f("volumeMm3"),
com_mm,
area_mm2: f("surfaceAreaMm2"),
tol_pct,
script,
needs_ops,
simulator_skips,
derivation,
id,
})
}
/// `derivation`: a list of `[name, expression]` pairs; a name is an
/// identifier (it becomes a Python variable), the last ones the key's
/// quantities.
fn derivation_pairs(v: &Value) -> Result<Vec<(String, String)>, String> {
let Some(d) = v.get("derivation").filter(|d| !d.is_null()) else { return Ok(Vec::new()) };
let bad = || "derivation is a list of [name, Python expression] pairs".to_string();
let mut out = Vec::new();
for pair in d.as_array().ok_or_else(bad)? {
match pair.as_array().map(|a| a.as_slice()) {
Some([Value::String(n), Value::String(e)])
if !n.is_empty() && n.chars().next().map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false)
&& n.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !e.trim().is_empty() =>
{
out.push((n.clone(), e.clone()))
}
_ => return Err(bad()),
}
}
if !out.iter().any(|(n, _)| n == "massG") {
return Err("a derivation must end in massG (the key)".into());
}
Ok(out)
}
/// Every shipped spec, parsed.
pub fn specs() -> Vec<Result<Spec, String>> {
let raw: Vec<Result<Value, String>> = SPECS.iter().map(|(name, text)| serde_json::from_str::<Value>(text).map_err(|e| format!("{name}: {e}"))).collect();
let all: Vec<Value> = raw.iter().filter_map(|r| r.as_ref().ok().cloned()).collect();
raw.into_iter().map(|r| r.and_then(|v| expand(&v, &all)).and_then(|v| parse_spec(&v))).collect()
}
/// How far off `got` is, in percent of `want`.
pub fn pct_off(got: f64, want: f64) -> f64 {
if want == 0.0 {
return if got == 0.0 { 0.0 } else { f64::INFINITY };
}
(got - want).abs() / want.abs() * 100.0
}
/// A centre-of-mass coordinate is within tolerance when it is within
/// `tol_pct` of the key, or within 0.05 mm (a coordinate of 0 has no percent).
pub fn coord_ok(got: f64, want: f64, tol_pct: f64) -> bool {
(got - want).abs() <= (want.abs() * tol_pct / 100.0).max(0.05)
}
/// COMPARE a swx_mass_properties reply with the key. Mass always; volume,
/// centre of mass and surface area when the key has them — except that a
/// SIMULATED reply (the mock) is compared on mass and volume only, the two
/// quantities its arithmetic model gets exactly. Returns (pass, the checks).
pub fn compare(spec: &Spec, mp: &Value) -> (bool, Value) {
let simulated = mp.get("simulated").is_some();
// The simulator's centre of mass is compared when its column model made
// it (exact to the integration); its surface area never is.
let com_simulated_ok = mp.pointer("/simulatedBy/volume").and_then(|v| v.as_str()).map(|v| v.contains("column")).unwrap_or(false);
let mut checks = Vec::new();
let mut pass = true;
let mass = mp.get("massG").and_then(|m| m.as_f64()).or_else(|| mp.get("massKg").and_then(|m| m.as_f64()).map(|k| k * 1000.0));
let mut one = |what: &str, got: Option<f64>, want: f64| {
let off = got.map(|g| pct_off(g, want));
let ok = off.map(|o| o <= spec.tol_pct).unwrap_or(false);
pass &= ok;
checks.push(json!({"quantity": what, "expected": want, "measured": got, "offPct": off, "pass": ok}));
};
one("massG", mass, spec.mass_g);
if let Some(v) = spec.volume_mm3 {
let got = mp.get("volumeMm3").and_then(|m| m.as_f64()).or_else(|| mp.get("volumeM3").and_then(|m| m.as_f64()).map(|v| v * 1.0e9));
one("volumeMm3", got, v);
}
if let Some(a) = spec.area_mm2 {
if simulated {
checks.push(json!({"quantity": "surfaceAreaMm2", "expected": a, "pass": Value::Null, "skipped": "not compared in the simulator"}));
} else {
let got = mp.get("surfaceAreaMm2").and_then(|m| m.as_f64()).or_else(|| mp.get("surfaceAreaM2").and_then(|m| m.as_f64()).map(|v| v * 1.0e6));
one("surfaceAreaMm2", got, a);
}
}
if let Some(c) = spec.com_mm {
if simulated && !com_simulated_ok {
checks.push(json!({"quantity": "centerOfMassMm", "expected": c, "pass": Value::Null, "skipped": "not compared in the simulator"}));
} else {
let got: Vec<f64> = mp.get("centerOfMassMm").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|x| x.as_f64()).collect()).unwrap_or_default();
let ok = got.len() == 3 && (0..3).all(|i| coord_ok(got[i], c[i], spec.tol_pct));
pass &= ok;
checks.push(json!({"quantity": "centerOfMassMm", "expected": c, "measured": got, "pass": ok}));
}
}
(pass, Value::Array(checks))
}
/// The one-line verdict: "N of M CSWA parts within tolerance", and what was
/// skipped and why — parts needing ops the bridge lacks, and (in the
/// simulator) parts only SOLIDWORKS can build.
pub fn summary(passed: usize, ran: usize, skipped: usize, simulator_only: usize) -> String {
let mut s = format!("{passed} of {ran} CSWA parts within tolerance");
let mut why = Vec::new();
if skipped > 0 {
why.push(format!("{skipped} skipped: they need ops the bridge does not have yet"));
}
if simulator_only > 0 {
why.push(format!("{simulator_only} skipped in the simulator: only SOLIDWORKS can build them"));
}
if !why.is_empty() {
s.push_str(&format!(" ({})", why.join("; ")));
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_shipped_spec_parses_and_the_list_is_the_directory() {
for s in specs() {
assert!(s.is_ok(), "{s:?}");
}
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/cswa");
let mut on_disk: Vec<String> = std::fs::read_dir(dir).unwrap().filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().to_string())
.filter(|n| n.ends_with(".json")).collect();
on_disk.sort();
let mut listed: Vec<String> = SPECS.iter().map(|(n, _)| n.to_string()).collect();
listed.sort();
assert_eq!(on_disk, listed, "every resources/cswa/*.json is embedded, and nothing else");
// Spec 0 is the LEGO brick: Michael's measured volume.
let lego = specs().remove(0).unwrap();
assert_eq!(lego.id, "cswa-00-lego-2x4");
assert_eq!(lego.volume_mm3, Some(2073.3615208));
assert!((lego.mass_g - 2073.3615208 * 1.02e-3).abs() < 1e-6);
}
#[test]
fn variables_and_extends_expand_and_the_python_checker_agrees() {
let vars: std::collections::HashMap<String, f64> = [("A".to_string(), 81.0)].into_iter().collect();
assert_eq!(eval("A - 43", &vars).unwrap(), 38.0);
assert_eq!(eval("2^3^2", &vars).unwrap(), 512.0, "^ is right-associative");
assert_eq!(eval("-2^2", &vars).unwrap(), 4.0, "unary minus binds tighter, as in the Python twin");
assert!((eval("atan2(1, 1) - pi/4", &vars).unwrap()).abs() < 1e-15);
assert!((eval("sqrt(29^2 - 19^2) + rad(180) - pi", &vars).unwrap() - (29f64 * 29.0 - 361.0).sqrt()).abs() < 1e-12);
assert!(eval("B", &vars).unwrap_err().contains("unknown variable B"));
assert!(eval("sqrt(-1)", &vars).is_err(), "not finite");
assert!(eval("1 +", &vars).is_err() && eval("foo(1)", &vars).is_err() && eval("(1", &vars).is_err());
// Step 2 is Step 1's script with other variables; Step 3 appends cuts.
let all: Vec<Value> = SPECS.iter().map(|(_, t)| serde_json::from_str(t).unwrap()).collect();
let by = |id: &str| all.iter().find(|v| v["id"] == id).unwrap();
let s1 = expand(by("cswa-05-tool-block-step1"), &all).unwrap();
let s2 = expand(by("cswa-06-tool-block-step2"), &all).unwrap();
let s3 = expand(by("cswa-07-tool-block-step3"), &all).unwrap();
assert_eq!(s1["script"][2]["depthMm"], 43.0);
assert_eq!(s2["script"][2]["depthMm"], 45.0);
assert_eq!(s2["script"].as_array().unwrap().len(), s1["script"].as_array().unwrap().len());
assert_eq!(s3["script"].as_array().unwrap().len(), s1["script"].as_array().unwrap().len() + 7);
// The recipe's R19 centre (21.54, 33.49) and tangent point (18.24, 14.78):
// the tangent arc continues the 10° line that ends at T.
let t = &s1["script"][1]["entities"][0]["line"]["to"];
assert!((t[0].as_f64().unwrap() - 18.243084987875623).abs() < 1e-9 && (t[1].as_f64().unwrap() - 14.783251905279968).abs() < 1e-9, "{t}");
// tools/cswa-check.py implements the same grammar: it must expand
// every spec to the same numbers (when python3 is there to ask).
let out = std::process::Command::new("python3")
.arg(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tools/cswa-check.py"))
.arg("--expand-only")
.output();
if let Some(o) = out.ok().filter(|o| o.status.success()) {
let py: Value = serde_json::from_slice(&o.stdout).unwrap();
for v in &all {
let id = v["id"].as_str().unwrap();
let rs = expand(v, &all).unwrap();
let close = |a: &Value, b: &Value| -> bool {
fn walk(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(x), Value::Number(y)) => (x.as_f64().unwrap() - y.as_f64().unwrap()).abs() <= 1e-9 * x.as_f64().unwrap().abs().max(1.0),
(Value::Array(x), Value::Array(y)) => x.len() == y.len() && x.iter().zip(y).all(|(p, q)| walk(p, q)),
(Value::Object(x), Value::Object(y)) => x.len() == y.len() && x.iter().all(|(k, p)| y.get(k).map(|q| walk(p, q)).unwrap_or(false)),
_ => a == b,
}
}
walk(a, b)
};
assert!(close(&rs["script"], &py[id]["script"]), "{id}: the Python checker expands the script differently");
assert!(close(&rs["expected"], &py[id]["expected"]), "{id}: ...and the key");
}
}
}
#[test]
fn an_appended_derived_value_may_not_shadow_the_base_s() {
let base = json!({"id": "b", "expected": {"massG": 1}, "variables": {"A": 2}, "derived": [["k", "A*2"]],
"script": [{"op": "new_part"}, {"op": "sketch_rect", "widthMm": "=k", "heightMm": 1}]});
let child = json!({"id": "c", "extends": "b", "derivedAppend": [["k", "A*3"]], "scriptAppend": []});
let all = vec![base.clone(), child.clone()];
let e = expand(&child, &all).unwrap_err();
assert!(e.contains("redefines \"k\"") && e.contains("b already derives"), "{e}");
// ...a new name is fine, and the base's script keeps its value.
let ok = json!({"id": "c", "extends": "b", "derivedAppend": [["k2", "A*3"]], "scriptAppend": [{"op": "sketch_rect", "widthMm": "=k2", "heightMm": 1}]});
let all = vec![base, ok.clone()];
let x = expand(&ok, &all).unwrap();
assert_eq!(x["script"][1]["widthMm"], 4.0);
assert_eq!(x["script"][2]["widthMm"], 6.0);
// The Python twin refuses it too.
let dir = std::env::temp_dir().join(format!("swx-cswa-shadow-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("resources/cswa")).unwrap();
std::fs::create_dir_all(dir.join("tools")).unwrap();
std::fs::copy(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tools/cswa-check.py"), dir.join("tools/cswa-check.py")).unwrap();
std::fs::write(dir.join("resources/cswa/b.json"), json!({"id": "b", "expected": {"massG": 1}, "variables": {"A": 2}, "derived": [["k", "A*2"]], "script": []}).to_string()).unwrap();
std::fs::write(dir.join("resources/cswa/c.json"), json!({"id": "c", "extends": "b", "derivedAppend": [["k", "A*3"]]}).to_string()).unwrap();
if let Ok(o) = std::process::Command::new("python3").arg(dir.join("tools/cswa-check.py")).arg("--expand-only").output() {
assert!(!o.status.success() && String::from_utf8_lossy(&o.stderr).contains("redefines 'k'"), "{}", String::from_utf8_lossy(&o.stderr));
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_reply_is_compared_within_the_tolerance() {
let spec = parse_spec(&json!({"id": "t", "expected": {"massG": 100.0, "volumeMm3": 1000.0, "centerOfMassMm": [10.0, 0.0, -5.0], "surfaceAreaMm2": 500.0},
"script": [{"op": "new_part"}]})).unwrap();
let good = json!({"massG": 100.9, "volumeMm3": 1009.0, "centerOfMassMm": [10.09, 0.04, -5.04], "surfaceAreaMm2": 504.0});
assert!(compare(&spec, &good).0, "{}", compare(&spec, &good).1);
let off = json!({"massG": 101.1, "volumeMm3": 1000.0, "centerOfMassMm": [10.0, 0.0, -5.0], "surfaceAreaMm2": 500.0});
assert!(!compare(&spec, &off).0, "1.1 % off is out");
let com = json!({"massG": 100.0, "volumeMm3": 1000.0, "centerOfMassMm": [10.0, 0.06, -5.0], "surfaceAreaMm2": 500.0});
assert!(!compare(&spec, &com).0, "0.06 mm off a zero coordinate is out");
// massKg is read when massG is missing.
let kg = json!({"massKg": 0.1, "volumeM3": 1.0e-6, "centerOfMassMm": [10.0, 0.0, -5.0], "surfaceAreaM2": 5.0e-4});
assert!(compare(&spec, &kg).0, "{}", compare(&spec, &kg).1);
// The simulator is held to mass and volume only.
let sim = json!({"massG": 100.0, "volumeMm3": 1000.0, "simulated": "mock", "centerOfMassMm": [0, 0, 0], "surfaceAreaMm2": 1.0});
assert!(compare(&spec, &sim).0);
// ...except that the column model's centre of mass IS compared.
let col = json!({"massG": 100.0, "volumeMm3": 1000.0, "simulated": "mock", "simulatedBy": {"volume": "column integration"}, "centerOfMassMm": [0, 0, 0], "surfaceAreaMm2": 1.0});
assert!(!compare(&spec, &col).0, "a wrong centre of mass from the column model fails");
assert!(!compare(&spec, &json!({})).0, "nothing measured is not a pass");
assert_eq!(summary(3, 4, 1, 0), "3 of 4 CSWA parts within tolerance (1 skipped: they need ops the bridge does not have yet)");
assert_eq!(summary(4, 4, 0, 1), "4 of 4 CSWA parts within tolerance (1 skipped in the simulator: only SOLIDWORKS can build them)");
assert_eq!(summary(2, 2, 1, 2), "2 of 2 CSWA parts within tolerance (1 skipped: they need ops the bridge does not have yet; 2 skipped in the simulator: only SOLIDWORKS can build them)");
assert_eq!(summary(5, 5, 0, 0), "5 of 5 CSWA parts within tolerance");
}
#[test]
fn a_spec_is_refused_for_what_would_make_it_meaningless() {
assert!(parse_spec(&json!({"id": "a", "expected": {}, "script": []})).unwrap_err().contains("massG"));
assert!(parse_spec(&json!({"id": "a", "units": "IPS", "expected": {"massG": 1}, "script": []})).unwrap_err().contains("MMGS"));
assert!(parse_spec(&json!({"id": "a", "expected": {"massG": 1}, "script": [{"op": "frobnicate"}]})).unwrap_err().contains("does not parse"));
// A spec that needs a missing op is not parsed as a script: it is skipped.
assert!(parse_spec(&json!({"id": "a", "expected": {"massG": 1}, "needsOps": ["loft"], "script": [{"op": "loft"}]})).is_ok());
assert!(parse_spec(&json!({"id": "a", "expected": {"massG": 1, "tolerancePct": 9}, "script": [{"op": "new_part"}]})).unwrap_err().contains("tolerancePct"));
// A derivation must be [name, expression] pairs and end in the key's mass (0.11.4).
let with = |d: Value| parse_spec(&json!({"id": "a", "expected": {"massG": 1}, "script": [{"op": "new_part"}], "derivation": d}));
assert!(with(json!([["V", "1"]])).unwrap_err().contains("must end in massG"));
assert!(with(json!([["massG"]])).unwrap_err().contains("[name, Python expression]"));
assert!(with(json!([["1x", "1"], ["massG", "1"]])).unwrap_err().contains("[name, Python expression]"), "a name is an identifier");
assert!(with(json!({"massG": "1"})).unwrap_err().contains("[name, Python expression]"));
assert_eq!(with(json!([["V", "2*3"], ["massG", "V*1e-3"]])).unwrap().derivation, vec![("V".to_string(), "2*3".to_string()), ("massG".to_string(), "V*1e-3".to_string())]);
assert!(parse_spec(&json!({"id": "a", "expected": {"massG": 1}, "script": [], "simulatorSkips": 3})).unwrap_err().contains("simulatorSkips"));
}
/// What is true of the BASE part only — the derivation of ITS key, why IT
/// is skipped in the simulator, the ops it lacked — is not inherited by a
/// spec that extends it (0.11.4).
#[test]
fn a_spec_that_extends_another_does_not_inherit_its_derivation_or_its_skips() {
let base = json!({"id": "b", "expected": {"massG": 2}, "script": [{"op": "new_part"}], "derivation": [["massG", "2"]],
"simulatorSkips": "only SOLIDWORKS", "needsOps": ["loft"]});
let child = json!({"id": "c", "extends": "b", "expected": {"massG": 3}});
let all = vec![base, child.clone()];
let x = expand(&child, &all).unwrap();
assert!(x.get("derivation").is_none() && x.get("simulatorSkips").is_none() && x.get("needsOps").is_none(), "{x}");
let s = parse_spec(&x).unwrap();
assert!(s.derivation.is_empty() && s.simulator_skips.is_none() && s.needs_ops.is_empty());
// ...while its own are its own.
let own = json!({"id": "c", "extends": "b", "expected": {"massG": 3}, "simulatorSkips": "mine"});
let all = vec![all[0].clone(), own.clone()];
assert_eq!(parse_spec(&expand(&own, &all).unwrap()).unwrap().simulator_skips.as_deref(), Some("mine"));
}
/// The Python checker REFUSES a derivation that does not reproduce its
/// key (0.11.4) — run on a scratch copy holding one wrong spec.
#[test]
fn a_derivation_that_misses_its_key_is_reported() {
let dir = std::env::temp_dir().join(format!("swx-cswa-derivation-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("resources/cswa")).unwrap();
std::fs::create_dir_all(dir.join("tools")).unwrap();
std::fs::copy(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tools/cswa-check.py"), dir.join("tools/cswa-check.py")).unwrap();
let spec = |mass: f64, com: Value| json!({"id": "d", "expected": {"massG": mass, "volumeMm3": 1000.0, "centerOfMassMm": com},
"derivation": [["volumeMm3", "10**3"], ["massG", "volumeMm3*2.7e-3"], ["centerOfMassMm", "[5, 5, 5]"]], "script": []});
let run = |v: &Value| -> Option<(bool, String)> {
std::fs::write(dir.join("resources/cswa/d.json"), v.to_string()).unwrap();
let o = std::process::Command::new("python3").arg(dir.join("tools/cswa-check.py")).arg("--derivations").output().ok()?;
Some((o.status.success(), String::from_utf8_lossy(&o.stdout).to_string()))
};
if let Some((ok, out)) = run(&spec(2.7, json!([5.0, 5.0, 5.0]))) {
assert!(ok, "the exact key is reproduced: {out}");
let (ok, out) = run(&spec(2.7001, json!([5.0, 5.0, 5.0]))).unwrap();
assert!(!ok && out.contains("massG: the derivation gives"), "a mass 1e-4 g off is reported: {out}");
let (ok, out) = run(&spec(2.7, json!([5.0, 5.0, 5.01]))).unwrap();
assert!(!ok && out.contains("centerOfMassMm"), "a centre of mass 0.01 mm off is reported: {out}");
}
let _ = std::fs::remove_dir_all(&dir);
}
}