Files
OneCAD/crates/doc/src/feature_walk_tests.rs
T
CamWight b6e87f0c44 Two tests pass on a Windows checkout: a CRLF parse and an 8.3 TEMP path (#1186) (#1211)
Make two tests pass on a Windows checkout: a CRLF parse and an 8.3 TEMP path.
2026-10-14 17:36:07 -04:00

198 lines
9.4 KiB
Rust

//! Golden records of what the walks over a feature report, for every variant (and every fillet
//! style, pattern, hole and construction definition):
//! - expressions: `inputs` (label, expression, kind, in order: the order names the `dN`
//! parameters) and `expressions` (every expression, placement points included). Each
//! expression field holds its own name, so a moved, missing or relabelled input shows;
//! - references: the sketches a feature is built from, and the fields `remap_refs` and
//! `remap_bodies` rewrite when everything is copied under new ids and names;
//! - geometry: the fields `transform_geometry` moves, the face points, the profile selections and
//! the placement point.
//!
//! The records live in `feature_walk_tests/*.txt`; to record an intended change, run the tests
//! with `FEATURE_WALK_BLESS=1`.
mod fixtures;
use std::collections::BTreeMap;
use fixtures::{changed, fixtures, with_points, with_refs};
use crate::FeatureKind;
/// Per fixture: its inputs, one per line (`label = expression (kind)`), then every expression.
fn record() -> String {
let mut out = String::new();
for (name, v) in fixtures() {
let kind: FeatureKind = serde_json::from_value(v).unwrap_or_else(|e| panic!("{name}: {e}"));
out += &format!("{name}\n");
let inputs = kind.inputs();
if !inputs.is_empty() {
out += " inputs:\n";
for (label, e, k) in inputs {
out += &format!(" {label} = {e} ({k:?})\n");
}
}
let expressions = kind.expressions();
if !expressions.is_empty() {
out += &format!(" expressions: {}\n", expressions.join(", "));
}
}
out
}
/// Per fixture: the sketches it is built from, and the fields that copying with every sketch,
/// feature and body renamed rewrites (`remap_refs`: sketches and features; `remap_bodies`).
fn ref_record() -> String {
let sketches: BTreeMap<u64, u64> = (1..=8).map(|i| (i, i + 100)).collect();
let names = BTreeMap::from([("F".to_string(), "F2".to_string()), ("CP".to_string(), "CP2".to_string())]);
let bodies = BTreeMap::from([("B".to_string(), "B2".to_string())]);
let mut out = String::new();
for (name, v) in fixtures() {
let kind: FeatureKind = serde_json::from_value(with_refs(name, v)).unwrap_or_else(|e| panic!("{name}: {e}"));
let before = serde_json::to_value(&kind).unwrap();
let mut refs = kind.clone();
refs.remap_refs(&sketches, &names);
let mut by_refs = Vec::new();
changed(&before, &serde_json::to_value(&refs).unwrap(), "", &mut by_refs);
let mut body = kind.clone();
body.remap_bodies(&bodies);
let mut by_bodies = Vec::new();
changed(&before, &serde_json::to_value(&body).unwrap(), "", &mut by_bodies);
out += &format!("{name}\n");
let sketches = kind.input_sketches();
if !sketches.is_empty() {
out += &format!(" sketches: {sketches:?}\n");
}
if !by_refs.is_empty() {
out += &format!(" remap_refs: {}\n", by_refs.join(", "));
}
if !by_bodies.is_empty() {
out += &format!(" remap_bodies: {}\n", by_bodies.join(", "));
}
}
out
}
/// A variant with a field that refers to a sketch (a field named `*sketch*` holding an id, sketch
/// points or loft sections) must report it from `input_sketches`, or deleting the sketch leaves
/// the feature broken. New variants are caught by the walk's exhaustive match; this catches a
/// sketch field added to an existing variant (its fixture has to set it).
/// Per fixture (with every reference and position set): the fields `transform_geometry` changes
/// under a turn and a shift (every point, direction and plane it maps), the face points that name
/// faces, the profile selections with their sketches, and a primitive's placement point.
fn geometry_record() -> String {
// x → y → z → x, then (1, 2, 3): every point and every direction of the fixtures changes.
let m = [[0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 2.0, 3.0, 1.0]];
let mut out = String::new();
for (name, v) in fixtures() {
let kind: FeatureKind = serde_json::from_value(with_points(name, with_refs(name, v))).unwrap_or_else(|e| panic!("{name}: {e}"));
let mut moved = kind.clone();
moved.transform_geometry(&m);
let mut paths = Vec::new();
changed(&serde_json::to_value(&kind).unwrap(), &serde_json::to_value(&moved).unwrap(), "", &mut paths);
out += &format!("{name}\n");
if !paths.is_empty() {
out += &format!(" moves: {}\n", paths.join(", "));
}
let faces: Vec<String> = kind.face_points().iter().map(|p| format!("[{}, {}, {}]", p.x, p.y, p.z)).collect();
if !faces.is_empty() {
out += &format!(" face points: {}\n", faces.join(", "));
}
let profiles: Vec<String> = kind
.profile_sels()
.iter()
.map(|(sketch, sel)| format!("{sketch} {}", serde_json::to_value(sel).unwrap()["type"].as_str().unwrap_or("?")))
.collect();
if !profiles.is_empty() {
out += &format!(" profiles: {}\n", profiles.join(", "));
}
if let Some(p) = kind.point_expr() {
out += &format!(" placement: {}\n", p.join(", "));
}
}
out
}
/// Compares `got` with the golden file `name` in `feature_walk_tests/`, line by line. With
/// `FEATURE_WALK_BLESS` set it rewrites the file instead, for an intended change (say why in the
/// pull request).
fn check_golden(name: &str, want: &str, got: &str) {
if std::env::var_os("FEATURE_WALK_BLESS").is_some() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/feature_walk_tests").join(name);
std::fs::write(&path, got).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
return;
}
for (line, (want, got)) in want.lines().zip(got.lines()).enumerate() {
assert_eq!(want, got, "{name} line {}; rerun with FEATURE_WALK_BLESS=1 if the change is intended", line + 1);
}
assert_eq!(want.lines().count(), got.lines().count(), "{name}: the record has a different number of lines");
}
/// Recorded from the hand-written `inputs_mut` and `expressions` the walk replaced.
const GOLDEN_EXPRS: &str = include_str!("feature_walk_tests/golden_exprs.txt");
/// Recorded from the hand-written `transform_geometry`, `face_points`, `profile_sels` and
/// `point_expr` the walk replaced.
const GOLDEN_GEOMETRY: &str = include_str!("feature_walk_tests/golden_geometry.txt");
/// Recorded from the hand-written `input_sketches`, `remap_refs` and `remap_bodies` the walk
/// replaced, with two fixes: a coil's `targets` and a surface trim's plane are remapped too.
const GOLDEN_REFS: &str = include_str!("feature_walk_tests/golden_refs.txt");
#[test]
fn input_sketches_lists_every_variant_with_a_sketch_field() {
let src = include_str!("document.rs").replace("\r\n", "\n");
let body = src.split("pub enum FeatureKind {").nth(1).and_then(|s| s.split("\n}\n").next()).unwrap();
let mut with_sketch = Vec::new();
let mut variant = String::new();
for line in body.lines() {
let t = line.trim();
if line.starts_with(" ") && !line.starts_with(" ") && t.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
variant = t.split(|c: char| !c.is_alphanumeric()).next().unwrap_or("").to_lowercase();
continue;
}
let Some((name, ty)) = t.strip_prefix("pub ").unwrap_or(t).split_once(':') else { continue };
let refers = (name.contains("sketch") && ty.contains("u64")) || ty.contains("SketchPoints") || ty.contains("LoftSection");
if refers && !t.starts_with("//") && !with_sketch.contains(&variant) {
with_sketch.push(variant.clone());
}
}
assert!(with_sketch.len() >= 10, "the FeatureKind parse found too few sketch users: {with_sketch:?}");
for v in with_sketch {
let uses = fixtures().into_iter().filter(|(_, f)| f["type"].as_str().unwrap().replace('_', "") == v).any(|(name, f)| {
let kind: FeatureKind = serde_json::from_value(with_refs(name, f)).unwrap();
!kind.input_sketches().is_empty()
});
assert!(uses, "FeatureKind::{v} has a sketch field but input_sketches reports no sketch for its fixtures");
}
}
#[test]
fn geometry_matches_the_golden_record() {
check_golden("golden_geometry.txt", GOLDEN_GEOMETRY, &geometry_record());
}
#[test]
fn references_match_the_golden_record() {
check_golden("golden_refs.txt", GOLDEN_REFS, &ref_record());
}
#[test]
fn inputs_and_expressions_match_the_golden_record() {
check_golden("golden_exprs.txt", GOLDEN_EXPRS, &record());
}
#[test]
fn the_record_covers_every_variant() {
let src = include_str!("document.rs").replace("\r\n", "\n");
let body = src.split("pub enum FeatureKind {").nth(1).and_then(|s| s.split("\n}\n").next()).unwrap();
let names: Vec<String> = fixtures().into_iter().map(|(_, v)| v["type"].as_str().unwrap().replace('_', "")).collect();
for line in body.lines().filter(|l| l.starts_with(" ") && !l.starts_with(" ")) {
let t = line.trim();
if t.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
let variant = t.split(|c: char| !c.is_alphanumeric()).next().unwrap_or("").to_lowercase();
assert!(names.contains(&variant), "FeatureKind::{t} has no fixture in feature_walk_tests");
}
}
}