Material tests: the nine copies in material_properties go; the persistence file checks the library copy's card and formats 20 and 21 in one loop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-31 09:03:08 -04:00
committed by jyviko
parent 43551498dd
commit 6704d3094b
2 changed files with 16 additions and 234 deletions
@@ -77,6 +77,9 @@ fn omission_retains_null_clears_and_unrelated_material_edits_retain_cards() {
.and_then(|c| c.in_format(solidcraft_io::catalog::V1_FORMAT))
.unwrap_err();
assert!(error.to_string().contains("engineering cards"));
// A library copy carries the card as it is.
let copy = solidcraft_io::catalog::LibraryMaterial::from_material(&s.doc.user_materials[1]).unwrap();
assert_eq!(copy.physical_card, original);
s.execute("material.edit", &json!({"material":"Renamed","physical_card":null})).unwrap();
assert!(s.doc.user_materials[1].physical_card.is_none());
s.execute("edit.undo", &json!({})).unwrap();
@@ -137,15 +140,20 @@ fn configurations_rename_and_paste_new_keep_design_owned_material_semantics() {
assert!(loaded.configs.rows.iter().any(|r| r.cells.iter().any(|c| c == "Renamed candidate")));
}
/// Formats 20 (before engineering cards) and 21 (with them), as they were written (no pending
/// format list): materials without cards come in with their optical look unchanged, upgraded.
#[test]
fn format21_materials_without_cards_keep_their_optical_look() {
fn format20_and_21_materials_without_cards_keep_their_optical_look() {
let s = fixture();
let mut saved = serde_json::to_value(&*s.doc).unwrap();
saved["format"] = json!("solidcraft/21");
let loaded = Document::from_json(&saved.to_string()).unwrap();
assert_eq!(loaded.user_materials, s.doc.user_materials);
assert!(loaded.user_materials[0].physical_card.is_none());
assert_eq!(loaded.format, solidcraft_doc::format::format_string(solidcraft_doc::format::FORMAT));
for format in [20, 21] {
let mut saved = serde_json::to_value(&*s.doc).unwrap();
saved["format"] = json!(format!("solidcraft/{format}"));
saved.as_object_mut().unwrap().remove("format_pending");
let loaded = Document::from_json(&saved.to_string()).unwrap();
assert_eq!(loaded.user_materials, s.doc.user_materials, "format {format}");
assert!(loaded.user_materials[0].physical_card.is_none(), "format {format}");
assert_eq!(loaded.format, solidcraft_doc::format::format_string(solidcraft_doc::format::FORMAT), "format {format}");
}
}
#[test]
@@ -208,17 +216,6 @@ fn native_container_keeps_large_si_card_values_sources_conditions_and_density_se
assert_eq!(material["physical_validation"]["source_review_required"], json!(["fixture"]));
}
#[test]
fn format20_materials_without_cards_upgrade_to21_without_changing_their_optical_look() {
let s = fixture();
let mut saved = serde_json::to_value(&*s.doc).unwrap();
saved["format"] = json!("solidcraft/20");
let loaded = Document::from_json(&saved.to_string()).unwrap();
assert_eq!(loaded.user_materials, s.doc.user_materials);
assert!(loaded.user_materials[0].physical_card.is_none());
assert_eq!(loaded.format, solidcraft_doc::format::format_string(solidcraft_doc::format::FORMAT));
}
#[test]
fn card_number_admission_preserves_metadata_without_exempting_other_material_fields() {
let s = fixture();
+1 -216
View File
@@ -44,7 +44,7 @@ fn validate(_: &mut Session, params: &Value) -> Result<Value> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{doc::Document, product_scene};
use crate::doc::Document;
use std::sync::Arc;
fn card() -> Value {
@@ -69,43 +69,6 @@ mod tests {
s
}
#[test]
fn card_edits_preserve_mass_optics_and_geometry_and_survive_undo_reopen() {
let mut s = fixture();
let scene = product_scene::extract(&s).unwrap();
let mass = s.execute("component.properties", &json!({})).unwrap();
let before_undo = s.undo.len();
let out = s.execute("material.edit", &json!({"material":"Synthetic","physical_card":card()})).unwrap();
assert_eq!(out["physical_validation"]["isotropic_moduli"]["shear_pa"], 4.9e9);
assert_eq!(out["density_comparison"]["assigned_density_kg_m3"], 800.0);
assert_eq!(out["density_comparison"]["matches_within_relative_tolerance"], false);
assert_eq!(out["physical_validation"]["source_review_required"], json!(["fixture"]));
assert_eq!(out["physical_validation"]["analysis_solver_available"], false);
assert_eq!(s.undo.len(), before_undo + 1);
assert_eq!(s.execute("component.properties", &json!({})).unwrap(), mass);
let after = product_scene::extract(&s).unwrap();
assert!(Arc::ptr_eq(&scene.meshes[0].mesh, &after.meshes[0].mesh));
assert_eq!(scene.meshes[0].surface, after.meshes[0].surface);
let saved = s.doc.to_json();
let saved_json: Value = serde_json::from_str(&saved).unwrap();
assert_eq!(saved_json["format"], crate::doc::format::format_string(crate::doc::format::FORMAT));
s.execute("edit.undo", &json!({})).unwrap();
assert!(s.doc.user_materials[0].physical_card.is_none());
s.execute("edit.redo", &json!({})).unwrap();
let mut reopened = Session::new(Document::from_json(&saved).unwrap());
assert_eq!(reopened.execute("component.properties", &json!({})).unwrap(), mass);
assert_eq!(reopened.doc.user_materials, s.doc.user_materials);
let modulus = reopened.doc.user_materials[0].physical_card.as_ref().unwrap().card().directional_modulus([1., 2., 3.]).unwrap();
assert_eq!(modulus.young_modulus_pa, 12.25e9);
let doc = reopened.doc.clone();
let before = (reopened.revision, reopened.undo.len(), reopened.camera, reopened.selection.clone());
let list = reopened.execute("material.list", &json!({})).unwrap();
let material = list["materials"].as_array().unwrap().iter().find(|m| m["name"] == "Synthetic").unwrap();
assert_eq!(material["physical_card"], out["physical_card"]);
assert!(Arc::ptr_eq(&doc, &reopened.doc));
assert_eq!(before, (reopened.revision, reopened.undo.len(), reopened.camera, reopened.selection.clone()));
}
#[test]
fn personal_format29_cards_upgrade_to_current_pending_format_without_changing_values() {
let mut s = fixture();
@@ -126,184 +89,6 @@ mod tests {
assert_eq!(session.execute("component.properties", &json!({})).unwrap(), mass);
}
#[test]
fn omission_retains_null_clears_and_unrelated_material_edits_retain_cards() {
let mut s = fixture();
s.execute("material.create", &json!({"name":"With card","density_g_cm3":0.7,"physical_card":card()})).unwrap();
let original = s.doc.user_materials[1].physical_card.clone();
for p in [json!({"density_g_cm3":0.9}), json!({"color":"#112233"}), json!({"name":"Renamed"})] {
let mut p = p;
p["material"] = json!(s.doc.user_materials[1].name);
s.execute("material.edit", &p).unwrap();
assert_eq!(s.doc.user_materials[1].physical_card, original);
}
let copy = solidcraft_io::catalog::LibraryMaterial::from_material(&s.doc.user_materials[1]).unwrap();
assert_eq!(copy.physical_card, original);
s.execute("material.edit", &json!({"material":"Renamed","physical_card":null})).unwrap();
assert!(s.doc.user_materials[1].physical_card.is_none());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc.user_materials[1].physical_card, original);
}
#[test]
fn hostile_cards_refuse_atomically_and_saved_card_admission_is_not_bypassed() {
let mut s = fixture();
let mut bad = card();
bad["properties"][1]["data"]["model"]["poisson_ratio"] = json!(0.6);
let mut oversized = card();
oversized["sources"] = json!(vec![json!({}); 33]);
let mut unknown = card();
unknown["unrecognized"] = json!(true);
let mut missing = card();
missing["properties"][0]["source_id"] = json!("missing");
let doc = s.doc.clone();
let before = (s.undo.len(), s.redo.len(), s.revision, s.camera, s.selection.clone());
for candidate in [bad, oversized, unknown, missing, json!(true)] {
assert!(s.execute("material.edit", &json!({"material":"Synthetic","physical_card":candidate})).is_err());
assert!(s.execute("material.create", &json!({"name":"Refused","density_g_cm3":1,"physical_card":candidate})).is_err());
assert!(Arc::ptr_eq(&doc, &s.doc));
assert_eq!(before, (s.undo.len(), s.redo.len(), s.revision, s.camera, s.selection.clone()));
let mut saved = serde_json::to_value(&*s.doc).unwrap();
saved["user_materials"][0]["physical_card"] = candidate;
assert!(Document::from_json(&saved.to_string()).is_err());
assert!(serde_json::from_value::<Document>(saved).is_err());
}
// Large finite SI constants are accepted only in this typed card, not geometry.
let mut saved = serde_json::to_value(&*s.doc).unwrap();
saved["user_materials"][0]["physical_card"] = card();
assert!(Document::from_json(&saved.to_string()).is_ok());
saved["hostile_numeric_probe"] = json!(12.25e9);
assert!(Document::from_json(&saved.to_string()).unwrap_err().to_string().contains("number"));
}
#[test]
fn configurations_rename_and_paste_new_keep_design_owned_material_semantics() {
let mut s = Session::default();
let part = s.execute("component.create", &json!({"name":"Part"})).unwrap()["component"].as_u64().unwrap();
s.execute("solid.box", &json!({"body_name":"Specimen","length":10,"width":10,"height":10})).unwrap();
s.execute("material.create", &json!({"name":"Synthetic","density_g_cm3":0.8,"physical_card":card()})).unwrap();
s.execute("config.configure", &json!({"materials":["Specimen"]})).unwrap();
s.execute("config.row", &json!({"name":"Candidate","values":{"material:Specimen":"Synthetic"}})).unwrap();
s.execute("config.activate", &json!({"row":"Candidate"})).unwrap();
s.execute("component.activate", &json!({"component":0})).unwrap();
s.execute("component.paste_new", &json!({"component":part,"translate":[30,0,0]})).unwrap();
assert_eq!(s.doc.user_materials.len(), 1);
assert_eq!(s.doc.materials.values().filter(|m| *m == "Synthetic").count(), 2);
let original = s.doc.user_materials[0].physical_card.clone();
s.execute("material.edit", &json!({"material":"Synthetic","name":"Renamed candidate"})).unwrap();
s.execute("config.activate", &json!({"row":"Default"})).unwrap();
s.execute("config.activate", &json!({"row":"Candidate"})).unwrap();
let loaded = Document::from_json(&s.doc.to_json()).unwrap();
assert_eq!(loaded.user_materials[0].physical_card, original);
assert_eq!(loaded.materials.values().filter(|m| *m == "Renamed candidate").count(), 2);
assert!(loaded.configs.rows.iter().any(|r| r.cells.iter().any(|c| c == "Renamed candidate")));
}
#[test]
fn format21_materials_without_cards_keep_their_optical_look() {
let s = fixture();
let mut saved = serde_json::to_value(&*s.doc).unwrap();
saved["format"] = json!("solidcraft/21");
saved.as_object_mut().unwrap().remove("format_pending");
let loaded = Document::from_json(&saved.to_string()).unwrap();
assert_eq!(loaded.user_materials, s.doc.user_materials);
assert!(loaded.user_materials[0].physical_card.is_none());
assert_eq!(loaded.format, crate::doc::format::format_string(crate::doc::format::FORMAT));
}
#[test]
fn scalar_catalog_refuses_card_export_without_creating_or_replacing_files_or_history() {
struct ProbeFile(std::path::PathBuf);
impl Drop for ProbeFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
let unique = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let file = ProbeFile(std::env::temp_dir().join(format!("solidcraft-card-refusal-{}-{unique}.json", std::process::id())));
let mut s = Session::default();
s.execute("material.create", &json!({"name":"Card only","density_g_cm3":0.8,"physical_card":card()})).unwrap();
s.mark_saved();
let doc = s.doc.clone();
let history = (s.undo.len(), s.redo.len(), s.revision);
for existing in [false, true] {
if existing {
std::fs::write(&file.0, b"keep existing catalog").unwrap();
}
for overwrite in [false, true] {
let error = s
.execute("library.export", &json!({"path":file.0,"format":solidcraft_io::catalog::V1_FORMAT,"overwrite":overwrite}))
.unwrap_err();
assert!(error.to_string().contains("cannot preserve PBR or engineering cards"));
if existing {
assert_eq!(std::fs::read(&file.0).unwrap(), b"keep existing catalog");
} else {
assert!(!file.0.exists());
}
assert!(Arc::ptr_eq(&doc, &s.doc));
assert_eq!((s.undo.len(), s.redo.len(), s.revision), history);
assert!(!s.is_dirty());
}
}
}
#[test]
fn native_container_keeps_large_si_card_values_sources_conditions_and_density_separate() {
let mut s = fixture();
s.execute("material.edit", &json!({"material":"Synthetic","physical_card":card()})).unwrap();
let before = s.execute("component.properties", &json!({})).unwrap();
let scene = product_scene::extract(&s).unwrap();
let native = solidcraft_io::write_design_container(&s.doc).unwrap();
assert!(solidcraft_io::is_design_container(&native));
let loaded = solidcraft_io::read_design(&native).unwrap();
assert_eq!(loaded.format, crate::doc::format::format_string(crate::doc::format::FORMAT));
assert_eq!(loaded.user_materials, s.doc.user_materials);
let mut reopened = Session::new(loaded);
assert_eq!(reopened.execute("component.properties", &json!({})).unwrap(), before);
assert_eq!(product_scene::extract(&reopened).unwrap().meshes[0].surface, scene.meshes[0].surface);
let list = reopened.execute("material.list", &json!({})).unwrap();
let material = list["materials"].as_array().unwrap().iter().find(|m| m["name"] == "Synthetic").unwrap();
assert_eq!(material["physical_card"], serde_json::to_value(s.doc.user_materials[0].physical_card.as_ref().unwrap()).unwrap());
assert_eq!(material["density_comparison"]["assigned_density_kg_m3"], 800.0);
assert_eq!(material["density_comparison"]["card_density_kg_m3"], 700.0);
assert_eq!(material["physical_validation"]["source_review_required"], json!(["fixture"]));
}
#[test]
fn format20_materials_without_cards_upgrade_without_changing_their_optical_look() {
let s = fixture();
let mut saved = serde_json::to_value(&*s.doc).unwrap();
saved["format"] = json!("solidcraft/20");
saved.as_object_mut().unwrap().remove("format_pending");
let loaded = Document::from_json(&saved.to_string()).unwrap();
assert_eq!(loaded.user_materials, s.doc.user_materials);
assert!(loaded.user_materials[0].physical_card.is_none());
assert_eq!(loaded.format, crate::doc::format::format_string(crate::doc::format::FORMAT));
}
#[test]
fn card_number_admission_preserves_metadata_without_exempting_other_material_fields() {
let s = fixture();
let mut saved = serde_json::to_value(&*s.doc).unwrap();
saved["user_materials"][0]["physical_card"] = card();
let metadata = json!({"org.example.measurement":{"value":12.25e9,"physical_card":{"untyped":12.25e9}}});
saved["metadata"] = metadata.clone();
let loaded = Document::from_json(&saved.to_string()).unwrap();
let reopened = solidcraft_io::read_design(&solidcraft_io::write_design_container(&loaded).unwrap()).unwrap();
assert_eq!(serde_json::to_value(&reopened.metadata).unwrap(), metadata);
assert_eq!(reopened.user_materials, loaded.user_materials);
// Only the exact root user-material card receives SI admission. The same key
// nested elsewhere must not exempt a hostile number from geometry limits.
for field in ["appearance", "unrecognized"] {
let mut hostile = saved.clone();
hostile["user_materials"][0][field] = json!({"physical_card":{"untyped":12.25e9}});
assert!(Document::from_json(&hostile.to_string()).unwrap_err().to_string().contains("number"));
}
saved["user_materials"][0]["density"] = json!(12.25e9);
assert!(Document::from_json(&saved.to_string()).unwrap_err().to_string().contains("number"));
}
#[test]
fn material_inspection_preserves_legacy_entries_and_reports_unknowns_and_density_separately() {
let mut s = fixture();