Objects: a test pins that both anchor accessors visit the same objects in the same order

Sorting and row or column edits read every anchor through
`object_anchors`, compute the new ones and write them back through
`object_anchors_mut` in step. Nothing checked that the two accessors
visit the same objects in the same order; a kind of floating object
reaching only one of them would have paired objects with other objects'
anchors silently.

Test: two objects of every kind are inserted in a mixed order, each
anchor is marked with its position through the mutable accessor, the
marks are read back through the shared one in the same sequence, the
count matches the per-kind collections, and the mark is found on the
object `object_anchor` names by kind and id. The test's constructor
matches on `ObjKind` exhaustively, so a new kind does not compile until
the test covers it.
This commit is contained in:
prxnvt
2026-11-01 16:38:23 -05:00
parent dd9a01e3b2
commit b874f58667
+87
View File
@@ -701,6 +701,93 @@ mod tests {
assert_eq!(s.col_at(-5.0), 0);
assert_eq!(s.col_left(2), 128.0);
}
use crate::features::{ChartKind, LegendPos, ShapeFormat, ShapeKind};
/// Adds a floating object of `kind` with id `id` at `anchor`. The match is exhaustive on
/// purpose: a new kind of floating object does not compile here until this test covers it,
/// and the counts in `object_anchor_accessors_visit_the_same_objects_in_the_same_order`
/// then catch an accessor that was not taught about it.
fn push_object(s: &mut Sheet, kind: ObjKind, id: u32, anchor: Anchor) {
match kind {
ObjKind::Chart => s.charts.push(Chart {
id,
kind: ChartKind::default(),
anchor,
title: None,
series: vec![],
legend: LegendPos::default(),
data_labels: false,
gridlines: false,
style: 0,
x_title: None,
y_title: None,
source: None,
by_rows: false,
pivot: None,
}),
ObjKind::Image => s.images.push(Image { id, anchor, data: Arc::new(vec![]), mime: String::new(), alt: String::new() }),
ObjKind::Shape => s.shapes.push(Shape {
id,
kind: ShapeKind::default(),
anchor,
fill: Color::default(),
line: Color::default(),
text: String::new(),
format: ShapeFormat::default(),
}),
ObjKind::Slicer => {
s.slicers.push(Slicer { id, table: String::new(), column: String::new(), caption: String::new(), anchor, selected: None, columns: 1 })
}
}
}
/// Sorting, and inserting or deleting rows and columns, read every anchor through
/// `object_anchors`, compute the new ones and write them back through `object_anchors_mut`
/// in step. The two must visit the same objects in the same order, or an object receives
/// another object's anchor; a kind that reaches only one of them would break that silently.
#[test]
fn object_anchor_accessors_visit_the_same_objects_in_the_same_order() {
let mut s = Sheet::new("S");
// Two objects of every kind, inserted in a mixed order, with ids unrelated to the kind.
// Each anchor cell's row carries the id, so an anchor read back names its object.
let inserted = [
(ObjKind::Slicer, 7),
(ObjKind::Chart, 3),
(ObjKind::Image, 11),
(ObjKind::Shape, 2),
(ObjKind::Chart, 9),
(ObjKind::Slicer, 1),
(ObjKind::Image, 4),
(ObjKind::Shape, 12),
];
for &(kind, id) in &inserted {
push_object(&mut s, kind, id, Anchor { cell: CellRef::new(id, 0), ..Default::default() });
}
let per_kind = s.charts.len() + s.images.len() + s.shapes.len() + s.slicers.len();
assert_eq!(per_kind, inserted.len());
assert_eq!(s.object_anchors().count(), per_kind, "object_anchors visits every collection");
assert_eq!(s.object_anchors_mut().count(), per_kind, "object_anchors_mut visits every collection");
// Mark each anchor with its position through the mutable accessor; the shared accessor
// reads the marks back in the same sequence.
for (i, a) in s.object_anchors_mut().enumerate() {
a.dx = i as f32;
}
let marks: Vec<f32> = s.object_anchors().map(|a| a.dx).collect();
assert_eq!(marks, (0..per_kind).map(|i| i as f32).collect::<Vec<f32>>());
// The sequence is the documented one (charts, images, shapes, slicers, each in insertion
// order) and every object appears in it once.
let documented: Vec<u32> =
ObjKind::ALL.iter().flat_map(|&k| inserted.iter().filter(move |(kind, _)| *kind == k).map(|&(_, id)| id)).collect();
let visited: Vec<u32> = s.object_anchors().map(|a| a.cell.row).collect();
assert_eq!(visited, documented);
// The mark written through the bulk accessor landed on the object `object_anchor` finds
// by kind and id.
for &(kind, id) in &inserted {
let position = documented.iter().position(|&x| x == id).map(|p| p as f32);
assert_eq!(s.object_anchor(ObjRef::new(kind, id)).map(|a| a.dx), position, "{kind:?} {id}");
}
}
}
// CellRef is a struct, so encode the sparse map as entries for JSON as well as other formats.