Files
OneCAD-Draft/crates/ocs_doc_api/tests/spec_wire_consistency.rs
T

417 lines
12 KiB
Rust

//! Spec↔wire consistency tests (enforces the codegen contract the headers
//! promise): every `op`/`query` name in `spec/entities.toml` maps to an existing
//! `Operation`/`Query` variant, and the wire enums are append-only relative to a
//! recorded baseline (bincode discriminant stability).
use std::collections::BTreeSet;
use serde::Deserialize;
#[derive(Deserialize)]
struct Spec {
#[serde(default)]
family: Vec<Family>,
}
#[derive(Deserialize)]
struct Family {
#[serde(default)]
handle: Option<String>,
#[serde(default)]
constructor: Vec<MethodRef>,
#[serde(default)]
method: Vec<MethodRef>,
}
#[derive(Deserialize)]
struct MethodRef {
#[serde(default)]
op: Option<String>,
#[serde(default)]
query: Option<String>,
}
fn spec() -> Spec {
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/spec/entities.toml"))
.expect("read spec/entities.toml");
toml::from_str(&src).expect("parse spec/entities.toml")
}
/// The variant names of the `Operation` enum, in declaration order.
fn operation_variants() -> Vec<String> {
use ocs_doc_api::ops::{
BoolOp, Curve2Spec, InsertSpec, LayerInfo, Operation, PlacementSpec, SolidPrimitive,
ViewportSpec,
};
use ocs_doc_api::ObjectId;
let _marker: Vec<Operation> = vec![
Operation::CreateSolid(SolidPrimitive::Sphere {
centre: [0.0; 3],
radius: 1.0,
}),
Operation::CreateCurve(Curve2Spec::Point {
position: [0.0; 3],
layer: None,
}),
Operation::Extrude {
profile: ObjectId::from_u64(0),
direction: [0.0; 3],
},
Operation::Revolve {
profile: ObjectId::from_u64(0),
axis: ([0.0; 3], [0.0; 3]),
angle: 0.0,
},
Operation::Transform {
id: ObjectId::from_u64(0),
placement: PlacementSpec::IDENTITY,
},
Operation::SolidBoolean {
op: BoolOp::Union,
a: ObjectId::from_u64(0),
b: ObjectId::from_u64(0),
erase_sources: true,
},
Operation::AddVertex {
id: ObjectId::from_u64(0),
at: 0,
point: [0.0; 3],
},
Operation::Delete {
id: ObjectId::from_u64(0),
},
Operation::CreateMany(vec![]),
Operation::TransformMany {
ids: vec![],
placement: PlacementSpec::IDENTITY,
},
Operation::DeleteMany(vec![]),
Operation::CreateInsert(InsertSpec {
block_name: String::new(),
insert_point: [0.0; 3],
scale: 1.0,
rotation: 0.0,
}),
Operation::CreateViewport(ViewportSpec {
center: [0.0; 3],
width: 0.0,
height: 0.0,
view_target: [0.0; 3],
view_height: 0.0,
}),
Operation::CreateText(ocs_doc_api::ops::TextSpec {
value: String::new(),
insertion_point: [0.0; 3],
height: 0.0,
rotation: 0.0,
}),
Operation::CreateMText(ocs_doc_api::ops::MTextSpec {
value: String::new(),
insertion_point: [0.0; 3],
height: 0.0,
}),
Operation::SetTextContent {
id: ObjectId::from_u64(0),
value: String::new(),
},
Operation::CreateHatch(ocs_doc_api::ops::HatchSpec {
boundary: vec![],
solid: true,
}),
Operation::CreateDimensionLinear(ocs_doc_api::ops::DimensionSpec {
first_point: [0.0; 3],
second_point: [0.0; 3],
definition_point: [0.0; 3],
}),
Operation::SetAttribute {
id: ObjectId::from_u64(0),
tag: String::new(),
value: String::new(),
},
Operation::SetViewportView {
id: ObjectId::from_u64(0),
view_target: [0.0; 3],
view_height: 0.0,
},
Operation::CreateRasterImage(ocs_doc_api::ops::RasterImageSpec {
file_path: String::new(),
insertion_point: [0.0; 3],
u_vector: [0.0; 3],
v_vector: [0.0; 3],
size: [0.0; 2],
}),
Operation::Loft { profiles: vec![] },
Operation::CreateDimensionRadius(ocs_doc_api::ops::DimensionRadialSpec {
center: [0.0; 3],
point: [0.0; 3],
}),
Operation::CreateDimensionDiameter(ocs_doc_api::ops::DimensionRadialSpec {
center: [0.0; 3],
point: [0.0; 3],
}),
Operation::CreateDimensionAngular(ocs_doc_api::ops::DimensionAngularSpec {
vertex: [0.0; 3],
first_point: [0.0; 3],
second_point: [0.0; 3],
arc_location: [0.0; 3],
}),
Operation::CreateAttributeDefinition(ocs_doc_api::ops::AttributeDefinitionSpec {
tag: String::new(),
prompt: String::new(),
default_value: String::new(),
insertion_point: [0.0; 3],
height: 0.0,
rotation: 0.0,
}),
Operation::CreateTable(ocs_doc_api::ops::TableSpec {
insertion_point: [0.0; 3],
data: vec![],
}),
Operation::CreateDimensionAngular2Ln(ocs_doc_api::ops::DimensionAngularSpec {
vertex: [0.0; 3],
first_point: [0.0; 3],
second_point: [0.0; 3],
arc_location: [0.0; 3],
}),
Operation::SetXData {
id: ObjectId::from_u64(0),
application_name: String::new(),
record: None,
},
Operation::SetXDataMany(vec![]),
Operation::CreateXRecord(ocs_doc_api::ops::XRecordSpec {
name: String::new(),
cloning_flags: ocs_doc_api::ops::XRecordCloningFlags::NotApplicable,
entries: vec![],
}),
Operation::SetXRecord {
id: ObjectId::from_u64(0),
spec: ocs_doc_api::ops::XRecordSpec {
name: String::new(),
cloning_flags: ocs_doc_api::ops::XRecordCloningFlags::NotApplicable,
entries: vec![],
},
},
Operation::CreateLayer(LayerInfo::new("LAYER")),
Operation::UpdateLayer {
name: String::new(),
info: LayerInfo::new("LAYER"),
},
Operation::DeleteLayer {
name: String::new(),
},
Operation::SetEntityLayer {
id: ObjectId::from_u64(0),
layer: String::new(),
},
];
_marker
.iter()
.enumerate()
.map(|(index, op)| {
let wire = bincode::serialize(op).unwrap();
assert_eq!(
u32::from_le_bytes(wire[..4].try_into().unwrap()) as usize,
index
);
let dbg = format!("{:?}", op);
dbg.split(['(', ' ', '{'])
.next()
.unwrap_or("")
.to_string()
})
.collect()
}
/// The variant names of the `Query` enum, in declaration order.
fn query_variants() -> Vec<String> {
use ocs_doc_api::query::Query;
use ocs_doc_api::ObjectId;
let marker: Vec<Query> = vec![
Query::GetEntity {
id: ObjectId::from_u64(0),
},
Query::GetBounds {
id: ObjectId::from_u64(0),
},
Query::GetCentroid {
id: ObjectId::from_u64(0),
},
Query::GetVolume {
id: ObjectId::from_u64(0),
},
Query::GetIntersects {
a: ObjectId::from_u64(0),
b: ObjectId::from_u64(0),
},
Query::GetGeometryRevision,
Query::GetTextContent {
id: ObjectId::from_u64(0),
},
Query::GetHatchBoundary {
id: ObjectId::from_u64(0),
},
Query::GetDimensionMeasurement {
id: ObjectId::from_u64(0),
},
Query::GetAttributes {
id: ObjectId::from_u64(0),
},
Query::GetBlockEntities {
block_name: String::new(),
},
Query::GetViewportView {
id: ObjectId::from_u64(0),
},
Query::GetXData {
id: ObjectId::from_u64(0),
application_name: String::new(),
},
Query::GetXRecord {
id: ObjectId::from_u64(0),
},
Query::ListLayers,
Query::GetEntityLayer {
id: ObjectId::from_u64(0),
},
];
marker
.iter()
.enumerate()
.map(|(index, q)| {
let wire = bincode::serialize(q).unwrap();
assert_eq!(
u32::from_le_bytes(wire[..4].try_into().unwrap()) as usize,
index
);
let dbg = format!("{:?}", q);
dbg.split(['(', ' ', '{'])
.next()
.unwrap_or("")
.to_string()
})
.collect()
}
#[test]
fn every_spec_op_and_query_maps_to_an_enum_variant() {
let spec = spec();
let op_names: BTreeSet<String> = operation_variants().into_iter().collect();
let query_names: BTreeSet<String> = query_variants().into_iter().collect();
let mut missing = Vec::new();
for fam in &spec.family {
for m in fam.constructor.iter().chain(fam.method.iter()) {
if let Some(op) = &m.op {
if !op_names.contains(op) {
missing.push(format!(
"Operation::{op} (family {})",
fam.handle.as_deref().unwrap_or("?")
));
}
}
if let Some(q) = &m.query {
if !query_names.contains(q) {
missing.push(format!(
"Query::{q} (family {})",
fam.handle.as_deref().unwrap_or("?")
));
}
}
}
}
assert!(
missing.is_empty(),
"spec references ops/queries with no enum variant: {missing:?}"
);
}
/// Recorded baseline of the `Operation` variant order (append-only contract).
/// New variants must be APPENDED after these; none may be removed or reordered.
const OPERATION_BASELINE: &[&str] = &[
"CreateSolid",
"CreateCurve",
"Extrude",
"Revolve",
"Transform",
"SolidBoolean",
"AddVertex",
"Delete",
"CreateMany",
"TransformMany",
"DeleteMany",
"CreateInsert",
"CreateViewport",
"CreateText",
"CreateMText",
"SetTextContent",
"CreateHatch",
"CreateDimensionLinear",
"SetAttribute",
"SetViewportView",
"CreateRasterImage",
"Loft",
"CreateDimensionRadius",
"CreateDimensionDiameter",
"CreateDimensionAngular",
"CreateAttributeDefinition",
"CreateTable",
"CreateDimensionAngular2Ln",
"SetXData",
"SetXDataMany",
"CreateXRecord",
"SetXRecord",
"CreateLayer",
"UpdateLayer",
"DeleteLayer",
"SetEntityLayer",
];
/// Recorded baseline of the `Query` variant order.
const QUERY_BASELINE: &[&str] = &[
"GetEntity",
"GetBounds",
"GetCentroid",
"GetVolume",
"GetIntersects",
"GetGeometryRevision",
"GetTextContent",
"GetHatchBoundary",
"GetDimensionMeasurement",
"GetAttributes",
"GetBlockEntities",
"GetViewportView",
"GetXData",
"GetXRecord",
"ListLayers",
"GetEntityLayer",
];
#[test]
fn operation_enum_is_append_only_vs_baseline() {
let current = operation_variants();
assert!(
current.len() >= OPERATION_BASELINE.len(),
"Operation enum shrank: {current:?} < baseline {OPERATION_BASELINE:?}"
);
for (i, name) in OPERATION_BASELINE.iter().enumerate() {
assert_eq!(
current[i], *name,
"Operation variant {i} changed (append-only violation): {current:?}"
);
}
}
#[test]
fn query_enum_is_append_only_vs_baseline() {
let current = query_variants();
assert!(
current.len() >= QUERY_BASELINE.len(),
"Query enum shrank: {current:?} < baseline {QUERY_BASELINE:?}"
);
for (i, name) in QUERY_BASELINE.iter().enumerate() {
assert_eq!(
current[i], *name,
"Query variant {i} changed (append-only violation): {current:?}"
);
}
}