forked from 1b-agent/opencadstudio-upstream
General improvements on API
This commit is contained in:
1 parent
56e09cd3f5
commit
55f61a4327
30 files changed
+1820
-2565
No files matched your search
@@ -1,5 +1,5 @@
|
||||
//! Unit tests over an in-memory mock `DocApiBackend`: no host.
|
||||
//! Covers the §9 examples: per-op atomicity, boolean erase_sources, OpGroup
|
||||
//! Unit tests over an in-memory mock `DocApiBackend` (plan §11.1): no host.
|
||||
//! Covers the §9 examples: per-op atomicity, boolean erase_sources, OpGroup
|
||||
//! compensation, bulk all-or-nothing, revision bumps, read-only query batching,
|
||||
//! and stale-handle `UnknownId` semantics.
|
||||
|
||||
@@ -17,30 +17,15 @@ use ocs_doc_api::{
|
||||
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||
match spec {
|
||||
Curve2Spec::Line { start, end } => Aabb {
|
||||
min: [
|
||||
start[0].min(end[0]),
|
||||
start[1].min(end[1]),
|
||||
start[2].min(end[2]),
|
||||
],
|
||||
max: [
|
||||
start[0].max(end[0]),
|
||||
start[1].max(end[1]),
|
||||
start[2].max(end[2]),
|
||||
],
|
||||
min: [start[0].min(end[0]), start[1].min(end[1]), start[2].min(end[2])],
|
||||
max: [start[0].max(end[0]), start[1].max(end[1]), start[2].max(end[2])],
|
||||
},
|
||||
Curve2Spec::Circle { centre, radius } | Curve2Spec::Arc { centre, radius, .. } => Aabb {
|
||||
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
||||
max: [centre[0] + radius, centre[1] + radius, centre[2]],
|
||||
},
|
||||
Curve2Spec::Point { position } => Aabb {
|
||||
min: *position,
|
||||
max: *position,
|
||||
},
|
||||
Curve2Spec::Polyline { points, .. }
|
||||
| Curve2Spec::Spline {
|
||||
control_points: points,
|
||||
..
|
||||
} => {
|
||||
Curve2Spec::Point { position } => Aabb { min: *position, max: *position },
|
||||
Curve2Spec::Polyline { points, .. } | Curve2Spec::Spline { control_points: points, .. } => {
|
||||
let mut min = [f64::INFINITY; 3];
|
||||
let mut max = [f64::NEG_INFINITY; 3];
|
||||
for p in points {
|
||||
@@ -51,28 +36,17 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||
}
|
||||
Aabb { min, max }
|
||||
}
|
||||
Curve2Spec::Ellipse {
|
||||
centre, major_axis, ..
|
||||
} => {
|
||||
let r = (major_axis[0] * major_axis[0]
|
||||
+ major_axis[1] * major_axis[1]
|
||||
+ major_axis[2] * major_axis[2])
|
||||
.sqrt();
|
||||
Aabb {
|
||||
min: [centre[0] - r, centre[1] - r, centre[2] - r],
|
||||
max: [centre[0] + r, centre[1] + r, centre[2] + r],
|
||||
}
|
||||
Curve2Spec::Ellipse { centre, major_axis, .. } => {
|
||||
let r = (major_axis[0] * major_axis[0] + major_axis[1] * major_axis[1] + major_axis[2] * major_axis[2]).sqrt();
|
||||
Aabb { min: [centre[0] - r, centre[1] - r, centre[2] - r], max: [centre[0] + r, centre[1] + r, centre[2] + r] }
|
||||
}
|
||||
Curve2Spec::Ray { origin, .. } | Curve2Spec::XLine { origin, .. } => Aabb {
|
||||
min: *origin,
|
||||
max: *origin,
|
||||
},
|
||||
Curve2Spec::Ray { origin, .. } | Curve2Spec::XLine { origin, .. } => Aabb { min: *origin, max: *origin },
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory mock backend: HashMap<ObjectId, Body> + a tiny entity table.
|
||||
/// Tracks undo steps + revision bumps to assert per-op semantics.
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Default)]
|
||||
struct MockBackend {
|
||||
next_id: u64,
|
||||
revision: u64,
|
||||
@@ -121,35 +95,6 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn create_many(&mut self, specs: &[ocs_doc_api::EntitySpec]) -> ApiResult<Vec<ObjectId>> {
|
||||
let mut next = self.clone();
|
||||
let ids = specs
|
||||
.iter()
|
||||
.map(|spec| match spec {
|
||||
ocs_doc_api::EntitySpec::Solid(spec) => {
|
||||
next.store_solid(&ocs_doc_api::geom::make_solid(spec)?)
|
||||
}
|
||||
ocs_doc_api::EntitySpec::Curve(spec) => {
|
||||
ocs_doc_api::convert::curve_spec_to_entity(spec)?;
|
||||
next.add_curve(spec)
|
||||
}
|
||||
})
|
||||
.collect::<ApiResult<Vec<_>>>()?;
|
||||
next.push_undo("CreateMany");
|
||||
next.finalize_op();
|
||||
*self = next;
|
||||
Ok(ids)
|
||||
}
|
||||
fn transform_many(&mut self, ids: &[ObjectId], placement: &PlacementSpec) -> ApiResult<()> {
|
||||
let mut next = self.clone();
|
||||
for &id in ids {
|
||||
next.transform_entity(id, placement)?;
|
||||
}
|
||||
next.push_undo("TransformMany");
|
||||
next.finalize_op();
|
||||
*self = next;
|
||||
Ok(())
|
||||
}
|
||||
fn add_curve(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId> {
|
||||
let kind = match spec {
|
||||
Curve2Spec::Line { .. } => "Line",
|
||||
@@ -174,8 +119,7 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
fn add_viewport(&mut self, spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Viewport");
|
||||
self.viewport_views
|
||||
.insert(id, (spec.view_target, spec.view_height));
|
||||
self.viewport_views.insert(id, (spec.view_target, spec.view_height));
|
||||
Ok(id)
|
||||
}
|
||||
fn add_text(&mut self, spec: &ocs_doc_api::ops::TextSpec) -> ApiResult<ObjectId> {
|
||||
@@ -197,27 +141,17 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
}
|
||||
fn text_content(&self, id: ObjectId) -> ApiResult<String> {
|
||||
self.text_values
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.ok_or(ApiError::UnknownId(id))
|
||||
self.text_values.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn add_hatch(&mut self, spec: &ocs_doc_api::ops::HatchSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Hatch");
|
||||
self.hatch_boundaries
|
||||
.insert(id, vec![spec.boundary.clone()]);
|
||||
self.hatch_boundaries.insert(id, vec![spec.boundary.clone()]);
|
||||
Ok(id)
|
||||
}
|
||||
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>> {
|
||||
self.hatch_boundaries
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.ok_or(ApiError::UnknownId(id))
|
||||
self.hatch_boundaries.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn add_dimension_linear(
|
||||
&mut self,
|
||||
spec: &ocs_doc_api::ops::DimensionSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
fn add_dimension_linear(&mut self, spec: &ocs_doc_api::ops::DimensionSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Dimension");
|
||||
let d = ((spec.first_point[0] - spec.second_point[0]).powi(2)
|
||||
+ (spec.first_point[1] - spec.second_point[1]).powi(2)
|
||||
@@ -226,54 +160,30 @@ impl DocApiBackend for MockBackend {
|
||||
self.dimension_measurements.insert(id, d);
|
||||
Ok(id)
|
||||
}
|
||||
fn add_dimension_radius(
|
||||
&mut self,
|
||||
spec: &ocs_doc_api::ops::DimensionRadialSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
fn add_dimension_radius(&mut self, spec: &ocs_doc_api::ops::DimensionRadialSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Dimension");
|
||||
let d = ((spec.center[0] - spec.point[0]).powi(2)
|
||||
+ (spec.center[1] - spec.point[1]).powi(2)
|
||||
+ (spec.center[2] - spec.point[2]).powi(2))
|
||||
.sqrt();
|
||||
let d = ((spec.center[0] - spec.point[0]).powi(2) + (spec.center[1] - spec.point[1]).powi(2) + (spec.center[2] - spec.point[2]).powi(2)).sqrt();
|
||||
self.dimension_measurements.insert(id, d);
|
||||
Ok(id)
|
||||
}
|
||||
fn add_dimension_diameter(
|
||||
&mut self,
|
||||
spec: &ocs_doc_api::ops::DimensionRadialSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
fn add_dimension_diameter(&mut self, spec: &ocs_doc_api::ops::DimensionRadialSpec) -> ApiResult<ObjectId> {
|
||||
self.add_dimension_radius(spec)
|
||||
}
|
||||
fn add_dimension_angular(
|
||||
&mut self,
|
||||
_spec: &ocs_doc_api::ops::DimensionAngularSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
fn add_dimension_angular(&mut self, _spec: &ocs_doc_api::ops::DimensionAngularSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Dimension");
|
||||
self.dimension_measurements.insert(id, 90.0); // mock: 90 degrees
|
||||
Ok(id)
|
||||
}
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
||||
self.dimension_measurements
|
||||
.get(&id)
|
||||
.copied()
|
||||
.ok_or(ApiError::UnknownId(id))
|
||||
self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn add_attribute_definition(
|
||||
&mut self,
|
||||
spec: &ocs_doc_api::ops::AttributeDefinitionSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
fn add_attribute_definition(&mut self, spec: &ocs_doc_api::ops::AttributeDefinitionSpec) -> ApiResult<ObjectId> {
|
||||
if spec.tag.is_empty() {
|
||||
return Err(ApiError::validation(
|
||||
"CreateAttributeDefinition",
|
||||
"empty tag",
|
||||
));
|
||||
return Err(ApiError::validation("CreateAttributeDefinition", "empty tag"));
|
||||
}
|
||||
Ok(self.alloc("AttributeDefinition"))
|
||||
}
|
||||
fn add_dimension_angular2ln(
|
||||
&mut self,
|
||||
_spec: &ocs_doc_api::ops::DimensionAngularSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
fn add_dimension_angular2ln(&mut self, _spec: &ocs_doc_api::ops::DimensionAngularSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Dimension");
|
||||
self.dimension_measurements.insert(id, 90.0); // mock: 90 degrees
|
||||
Ok(id)
|
||||
@@ -281,10 +191,7 @@ impl DocApiBackend for MockBackend {
|
||||
fn add_table(&mut self, spec: &ocs_doc_api::ops::TableSpec) -> ApiResult<ObjectId> {
|
||||
let cols = spec.data.first().map(|r| r.len()).unwrap_or(0);
|
||||
if spec.data.is_empty() || cols == 0 || spec.data.iter().any(|r| r.len() != cols) {
|
||||
return Err(ApiError::validation(
|
||||
"CreateTable",
|
||||
"table needs a non-empty rectangular grid",
|
||||
));
|
||||
return Err(ApiError::validation("CreateTable", "table needs a non-empty rectangular grid"));
|
||||
}
|
||||
Ok(self.alloc("Table"))
|
||||
}
|
||||
@@ -301,24 +208,15 @@ impl DocApiBackend for MockBackend {
|
||||
Ok(())
|
||||
}
|
||||
fn attributes(&self, id: ObjectId) -> ApiResult<Vec<(String, String)>> {
|
||||
self.attributes_store
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.ok_or(ApiError::UnknownId(id))
|
||||
self.attributes_store.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn block_entities(&self, _block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn add_raster_image(
|
||||
&mut self,
|
||||
_spec: &ocs_doc_api::ops::RasterImageSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
fn add_raster_image(&mut self, _spec: &ocs_doc_api::ops::RasterImageSpec) -> ApiResult<ObjectId> {
|
||||
Ok(self.alloc("RasterImage"))
|
||||
}
|
||||
fn loft(
|
||||
&mut self,
|
||||
sections: &[(cadkernel::space::Plane, Vec<cadkernel::geom2d::Curve>)],
|
||||
) -> ApiResult<ObjectId> {
|
||||
fn loft(&mut self, sections: &[Vec<cadkernel::geom2d::Curve>]) -> ApiResult<ObjectId> {
|
||||
if sections.len() < 2 {
|
||||
return Err(ApiError::validation("Loft", "loft needs >= 2 profiles"));
|
||||
}
|
||||
@@ -327,12 +225,7 @@ impl DocApiBackend for MockBackend {
|
||||
.ok_or_else(|| ApiError::geometry(GeometryErrorKind::InvalidInput, "make cuboid"))?;
|
||||
self.store_solid(&body)
|
||||
}
|
||||
fn set_viewport_view(
|
||||
&mut self,
|
||||
id: ObjectId,
|
||||
view_target: [f64; 3],
|
||||
view_height: f64,
|
||||
) -> ApiResult<()> {
|
||||
fn set_viewport_view(&mut self, id: ObjectId, view_target: [f64; 3], view_height: f64) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
@@ -340,10 +233,7 @@ impl DocApiBackend for MockBackend {
|
||||
Ok(())
|
||||
}
|
||||
fn viewport_view(&self, id: ObjectId) -> ApiResult<([f64; 3], f64)> {
|
||||
self.viewport_views
|
||||
.get(&id)
|
||||
.copied()
|
||||
.ok_or(ApiError::UnknownId(id))
|
||||
self.viewport_views.get(&id).copied().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
@@ -359,10 +249,7 @@ impl DocApiBackend for MockBackend {
|
||||
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
||||
let kind = self.kinds.get(&id).ok_or(ApiError::UnknownId(id))?.clone();
|
||||
let bounds = self.bodies.get(&id).and_then(|b| {
|
||||
cadkernel::brep::body_bounds(b).map(|bb| Aabb {
|
||||
min: bb.min,
|
||||
max: bb.max,
|
||||
})
|
||||
cadkernel::brep::body_bounds(b).map(|bb| Aabb { min: bb.min, max: bb.max })
|
||||
});
|
||||
Ok(EntityView { id, kind, bounds })
|
||||
}
|
||||
@@ -377,9 +264,8 @@ impl DocApiBackend for MockBackend {
|
||||
z_axis: placement.z_axis,
|
||||
origin: placement.origin,
|
||||
};
|
||||
let moved = cadkernel::brep::transform(&body, &place).ok_or_else(|| {
|
||||
ApiError::geometry(GeometryErrorKind::InvalidInput, "transform failed")
|
||||
})?;
|
||||
let moved = cadkernel::brep::transform(&body, &place)
|
||||
.ok_or_else(|| ApiError::geometry(GeometryErrorKind::InvalidInput, "transform failed"))?;
|
||||
self.bodies.insert(id, moved);
|
||||
}
|
||||
Ok(())
|
||||
@@ -396,10 +282,7 @@ impl DocApiBackend for MockBackend {
|
||||
let body = self.body(id)?;
|
||||
let bb = cadkernel::brep::body_bounds(body)
|
||||
.ok_or_else(|| ApiError::geometry(GeometryErrorKind::Empty, "no bounds"))?;
|
||||
Ok(Aabb {
|
||||
min: bb.min,
|
||||
max: bb.max,
|
||||
})
|
||||
Ok(Aabb { min: bb.min, max: bb.max })
|
||||
}
|
||||
fn centroid(&mut self, id: ObjectId) -> ApiResult<[f64; 3]> {
|
||||
let body = self.body(id)?;
|
||||
@@ -422,10 +305,9 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn api() -> (DocApi, Arc<ocs_doc_api::transport::InProcess<MockBackend>>) {
|
||||
let tp = Arc::new(ocs_doc_api::transport::InProcess::new(
|
||||
MockBackend::default(),
|
||||
));
|
||||
let tp = Arc::new(ocs_doc_api::transport::InProcess::new(MockBackend::default()));
|
||||
(DocApi::connect(tp.clone(), 0), tp)
|
||||
}
|
||||
|
||||
@@ -436,26 +318,13 @@ fn cuboid_intersect_cuboid_atomic_ops_and_revision() {
|
||||
let mut grp = ocs_doc_api::OpGroup::new();
|
||||
|
||||
let rev0 = doc.revision().unwrap();
|
||||
// Two overlapping boxes — the kernel's own boolean test case (boolean.rs pair()).
|
||||
let block = grp
|
||||
.track(
|
||||
doc.solids()
|
||||
.create_cuboid([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]),
|
||||
)
|
||||
.unwrap();
|
||||
let other = grp
|
||||
.track(
|
||||
doc.solids()
|
||||
.create_cuboid([5.0, 5.0, 5.0], [10.0, 10.0, 10.0]),
|
||||
)
|
||||
.unwrap();
|
||||
// Two overlapping boxes — the kernel's own boolean test case (boolean.rs pair()).
|
||||
let block = grp.track(doc.solids().create_cuboid([0.0, 0.0, 0.0], [10.0, 10.0, 10.0])).unwrap();
|
||||
let other = grp.track(doc.solids().create_cuboid([5.0, 5.0, 5.0], [10.0, 10.0, 10.0])).unwrap();
|
||||
assert_eq!(doc.revision().unwrap().as_u64(), rev0.as_u64() + 2);
|
||||
|
||||
let lens = match block.intersect(&other) {
|
||||
Ok(l) => {
|
||||
grp.commit();
|
||||
l
|
||||
}
|
||||
Ok(l) => { grp.commit(); l }
|
||||
Err(e) => panic!("intersect failed: {e}"),
|
||||
};
|
||||
assert_eq!(doc.revision().unwrap().as_u64(), rev0.as_u64() + 3);
|
||||
@@ -468,11 +337,8 @@ fn cuboid_intersect_cuboid_atomic_ops_and_revision() {
|
||||
.unwrap();
|
||||
let bb = res.bounds(0).unwrap();
|
||||
let vol = res.volume(1).unwrap();
|
||||
// Intersection of [0,10]³ ∩ [5,15]³ = [5,10]³ → bounds + volume 125.
|
||||
assert!(
|
||||
(bb.min[0] - 5.0).abs() < 1e-6 && (bb.max[0] - 10.0).abs() < 1e-6,
|
||||
"bounds {bb:?}"
|
||||
);
|
||||
// Intersection of [0,10]³ ∩ [5,15]³ = [5,10]³ → bounds + volume 125.
|
||||
assert!((bb.min[0] - 5.0).abs() < 1e-6 && (bb.max[0] - 10.0).abs() < 1e-6, "bounds {bb:?}");
|
||||
assert!(vol > 0.0, "intersection volume must be positive");
|
||||
}
|
||||
|
||||
@@ -480,14 +346,8 @@ fn cuboid_intersect_cuboid_atomic_ops_and_revision() {
|
||||
fn boolean_erase_sources_removes_second_solid() {
|
||||
let (api, tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
let a = doc
|
||||
.solids()
|
||||
.create_cuboid([0.0, 0.0, 0.0], [10.0; 3])
|
||||
.unwrap();
|
||||
let b = doc
|
||||
.solids()
|
||||
.create_cuboid([5.0, 5.0, 5.0], [10.0; 3])
|
||||
.unwrap();
|
||||
let a = doc.solids().create_cuboid([0.0, 0.0, 0.0], [10.0; 3]).unwrap();
|
||||
let b = doc.solids().create_cuboid([5.0, 5.0, 5.0], [10.0; 3]).unwrap();
|
||||
let result = a.union(&b).unwrap();
|
||||
let backend = tp.backend();
|
||||
assert!(backend.entity_exists(a.id()));
|
||||
@@ -508,12 +368,8 @@ fn opgroup_compensate_deletes_created() {
|
||||
let (api, tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
let mut grp = ocs_doc_api::OpGroup::new();
|
||||
let a = grp
|
||||
.track(doc.solids().create_cuboid([0.0, 0.0, 0.0], [10.0; 3]))
|
||||
.unwrap();
|
||||
let b = grp
|
||||
.track(doc.solids().create_sphere([5.0, 5.0, 5.0], 4.0))
|
||||
.unwrap();
|
||||
let a = grp.track(doc.solids().create_cuboid([0.0, 0.0, 0.0], [10.0; 3])).unwrap();
|
||||
let b = grp.track(doc.solids().create_sphere([5.0, 5.0, 5.0], 4.0)).unwrap();
|
||||
grp.compensate(&doc).unwrap();
|
||||
let backend = tp.backend();
|
||||
assert!(!backend.entity_exists(a.id()));
|
||||
@@ -537,10 +393,7 @@ fn bulk_create_many_one_undo_step() {
|
||||
fn transform_many_with_stale_id_fails_and_applies_nothing() {
|
||||
let (api, tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
let a = doc
|
||||
.solids()
|
||||
.create_cuboid([0.0, 0.0, 0.0], [10.0; 3])
|
||||
.unwrap();
|
||||
let a = doc.solids().create_cuboid([0.0, 0.0, 0.0], [10.0; 3]).unwrap();
|
||||
let rev_before = doc.revision().unwrap().as_u64();
|
||||
let stale = ObjectId::from_u64(424_242);
|
||||
let err = doc
|
||||
@@ -558,9 +411,7 @@ fn assert_revision_guard() {
|
||||
let doc = api.document(api.active_tab());
|
||||
let rev = doc.revision().unwrap();
|
||||
doc.assert_revision(rev).unwrap();
|
||||
doc.solids()
|
||||
.create_cuboid([0.0, 0.0, 0.0], [1.0; 3])
|
||||
.unwrap();
|
||||
doc.solids().create_cuboid([0.0, 0.0, 0.0], [1.0; 3]).unwrap();
|
||||
assert!(doc.assert_revision(rev).is_err());
|
||||
}
|
||||
|
||||
@@ -568,10 +419,7 @@ fn assert_revision_guard() {
|
||||
fn failed_boolean_leaves_inputs_live_and_no_undo() {
|
||||
let (api, tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
let a = doc
|
||||
.solids()
|
||||
.create_cuboid([0.0, 0.0, 0.0], [10.0; 3])
|
||||
.unwrap();
|
||||
let a = doc.solids().create_cuboid([0.0, 0.0, 0.0], [10.0; 3]).unwrap();
|
||||
// `b` is stale: boolean fails before push_undo -> no mutation, no undo step.
|
||||
let stale_solid = {
|
||||
// Build a Solid handle to a non-existent id via Entity downcast path.
|
||||
@@ -587,25 +435,3 @@ fn failed_boolean_leaves_inputs_live_and_no_undo() {
|
||||
assert!(backend.entity_exists(a.id()));
|
||||
assert_eq!(backend.revision, rev_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_handles_reject_foreign_sessions_and_tabs() {
|
||||
let first = DocApi::in_process(MockBackend::default(), 10);
|
||||
let second = DocApi::in_process(MockBackend::default(), 10);
|
||||
let doc = first.document(10);
|
||||
let foreign = second
|
||||
.document(10)
|
||||
.curves()
|
||||
.create_circle([0.0; 3], 2.0)
|
||||
.unwrap();
|
||||
assert!(doc.solids().extrude(&foreign, [0.0, 0.0, 2.0]).is_err());
|
||||
assert!(doc
|
||||
.solids()
|
||||
.loft(&[foreign.clone(), foreign.clone()])
|
||||
.is_err());
|
||||
assert!(doc.query_batch(|q| q.bounds(&foreign)).is_err());
|
||||
let mut group = ocs_doc_api::OpGroup::new();
|
||||
group.track(Ok(foreign)).unwrap();
|
||||
assert!(group.compensate(&doc).is_err());
|
||||
assert!(first.document(11).curves().create_point([0.0; 3]).is_err());
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Spec↔wire consistency tests (enforces the codegen contract the headers
|
||||
//! 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).
|
||||
@@ -37,151 +37,50 @@ fn spec() -> Spec {
|
||||
|
||||
/// The variant names of the `Operation` enum, in declaration order.
|
||||
fn operation_variants() -> Vec<String> {
|
||||
use ocs_doc_api::ops::{
|
||||
BoolOp, Curve2Spec, InsertSpec, Operation, PlacementSpec, SolidPrimitive, ViewportSpec,
|
||||
};
|
||||
use ocs_doc_api::ops::{BoolOp, Curve2Spec, InsertSpec, 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::CreateSolid(SolidPrimitive::Sphere { centre: [0.0; 3], radius: 1.0 }),
|
||||
Operation::CreateCurve(Curve2Spec::Point { position: [0.0; 3] }),
|
||||
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::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::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::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],
|
||||
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::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],
|
||||
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] }),
|
||||
];
|
||||
// Derive the variant names by serializing a probe of each and reading the
|
||||
// bincode variant index -> name map is not available; instead match by
|
||||
// constructing each and using a tiny name extractor via Debug prefix.
|
||||
_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
|
||||
);
|
||||
.map(|op| {
|
||||
let dbg = format!("{:?}", op);
|
||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{').next().unwrap_or("").to_string()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -191,56 +90,24 @@ 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::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::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) },
|
||||
];
|
||||
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
|
||||
);
|
||||
.map(|q| {
|
||||
let dbg = format!("{:?}", q);
|
||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{').next().unwrap_or("").to_string()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -256,26 +123,17 @@ fn every_spec_op_and_query_maps_to_an_enum_variant() {
|
||||
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("?")
|
||||
));
|
||||
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("?")
|
||||
));
|
||||
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:?}"
|
||||
);
|
||||
assert!(missing.is_empty(), "spec references ops/queries with no enum variant: {missing:?}");
|
||||
}
|
||||
|
||||
/// Recorded baseline of the `Operation` variant order (append-only contract).
|
||||
|
||||
Reference in new issue
Block a user