authorclover caruso <git@paperclover.net> 2026-10-10 20:04:13-07:00
committerclover caruso <git@paperclover.net> 2026-10-10 22:56:05-07:00
loge9d0ff1435aa931d38788bf676968c992e370104
treedeba8d70fb04411dcddb63f32d46d6253cc8f31a
parentde3ecb8964829cbf26e3abd78486bc59405b2ac4
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

test: preserve format invariants around revision attribution

Account for the revision metadata author and FILETIME while keeping untouched properties, object identities and historical revisions exact. Build checkpoint fault fixtures from the actual dependency depth, and verify first attribution checkpoints once with same-second precision retained. Assisted-by: gpt-6.1-sol

7 files changed, 316 insertions(+), 27 deletions(-)

crates/onestore/src/op/tests.rs+18-6
...@@ -2766,7 +2766,7 @@ fn a_page_date_stores_what_onenote_stores() {...@@ -2766,7 +2766,7 @@ fn a_page_date_stores_what_onenote_stores() {
2766 .apply(2766 .apply(
2767 "Author",2767 "Author",
2768 &Edit {2768 &Edit {
2769 at: AT,2769 at: AT + 12345,
2770 ops: vec![Op::Page { space: kept, op }],2770 ops: vec![Op::Page { space: kept, op }],
2771 },2771 },
2772 )2772 )
...@@ -2777,12 +2777,24 @@ fn a_page_date_stores_what_onenote_stores() {...@@ -2777,12 +2777,24 @@ fn a_page_date_stores_what_onenote_stores() {
2777 assert_eq!(stored.created, Some(DATED_AT));2777 assert_eq!(stored.created, Some(DATED_AT));
2778 assert_eq!(date_fields(&stored), fields);2778 assert_eq!(date_fields(&stored), fields);
2779 assert_eq!(stored, predicted);2779 assert_eq!(stored, predicted);
2780 assert_eq!(changed_jcids(&image, kept), native);2780 let mut expected = native;
2781 expected.extend([0x20044, 0x120001]);
2782 expected.sort();
2783 assert_eq!(changed_jcids(&image, kept), expected);
2781 let store = Store::parse(&image).unwrap();2784 let store = Store::parse(&image).unwrap();
2782 RevisionIndex::parse(&store)2785 let index = RevisionIndex::parse(&store).unwrap();
2783 .unwrap()2786 index.validate_current().unwrap();
2784 .validate_current()2787 let document = Document::parse(&index).unwrap();
2785 .unwrap();2788 let revision = document.active(kept).unwrap();
2789 let metadata = &revision.nodes[&revision.roots[&4]];
2790 assert!(matches!(
2791 metadata.kind,
2792 Kind::RevisionMetadata { modified_filetime: Some(at) } if at == AT + 12345
2793 ));
2794 assert!(matches!(
2795 &revision.nodes[&metadata.latest_author.unwrap()].kind,
2796 Kind::Author { name } if name.as_deref() == Some("Author")
2797 ));
27862798
2787 // The whole-page lowering reaches the same page through the same op.2799 // The whole-page lowering reaches the same page through the same op.
2788 let mut after = before.clone();2800 let mut after = before.clone();
crates/onestore/tests/edit.rs+30-5
...@@ -2,6 +2,8 @@...@@ -2,6 +2,8 @@
2mod checkpoint;2mod checkpoint;
3#[path = "support/disk.rs"]3#[path = "support/disk.rs"]
4mod disk;4mod disk;
5#[path = "support/metadata.rs"]
6mod metadata;
5#[path = "support/ops.rs"]7#[path = "support/ops.rs"]
6mod ops;8mod ops;
7#[path = "support/trace.rs"]9#[path = "support/trace.rs"]
...@@ -321,10 +323,24 @@ fn assert_other_objects_preserved(...@@ -321,10 +323,24 @@ fn assert_other_objects_preserved(
321 .find(|field| field.id == 0x14001d7a)323 .find(|field| field.id == 0x14001d7a)
322 .unwrap()324 .unwrap()
323 .value;325 .value;
326 let filetime = metadata::check(before, after, "Author");
327 let author = metadata::author(&after.objects[&after.roots[&4]]);
328 assert_eq!(
329 *modified,
330 onestore::Value::Bytes(
331 &u32::try_from(filetime / 10_000_000 - 11_960_006_400)
332 .unwrap()
333 .to_le_bytes()
334 )
335 );
324 let reachable = before.reachable().unwrap();336 let reachable = before.reachable().unwrap();
325 assert_eq!(before.objects.len(), after.objects.len());337 assert!(
338 after.objects.keys().all(|id| {
339 before.objects.contains_key(id) || *id == after.roots[&4] || *id == author
340 })
341 );
326 for (id, object) in &before.objects {342 for (id, object) in &before.objects {
327 if *id == edited {343 if *id == edited || Some(id) == before.roots.get(&4) {
328 continue;344 continue;
329 }345 }
330 let current = &after.objects[id];346 let current = &after.objects[id];
...@@ -334,7 +350,8 @@ fn assert_other_objects_preserved(...@@ -334,7 +350,8 @@ fn assert_other_objects_preserved(
334 current350 current
335 .global_ids351 .global_ids
336 .iter()352 .iter()
337 .all(|(entry, guid)| object.global_ids.get(entry) == Some(guid))353 .all(|(entry, guid)| object.global_ids.get(entry) == Some(guid)
354 || *guid == author.guid)
338 );355 );
339 let mut pending = vec![*id];356 let mut pending = vec![*id];
340 let mut visited = std::collections::BTreeSet::new();357 let mut visited = std::collections::BTreeSet::new();
...@@ -349,6 +366,10 @@ fn assert_other_objects_preserved(...@@ -349,6 +366,10 @@ fn assert_other_objects_preserved(
349 if ancestor {366 if ancestor {
350 allowed.push(0x14001d7a);367 allowed.push(0x14001d7a);
351 }368 }
369 if ancestor && object.jcid == 0x6000d {
370 allowed.push(0x20001d79);
371 assert_eq!(metadata::author(current), author);
372 }
352 if ancestor && object.jcid == 0x6000b {373 if ancestor && object.jcid == 0x6000b {
353 allowed.push(0x1c001d3c);374 allowed.push(0x1c001d3c);
354 }375 }
...@@ -368,7 +389,11 @@ fn assert_other_objects_preserved(...@@ -368,7 +389,11 @@ fn assert_other_objects_preserved(
368 if ancestor {389 if ancestor {
369 let old_time = old.sets[0].iter().find(|field| field.id == 0x14001d7a);390 let old_time = old.sets[0].iter().find(|field| field.id == 0x14001d7a);
370 let new_time = new.sets[0].iter().find(|field| field.id == 0x14001d7a);391 let new_time = new.sets[0].iter().find(|field| field.id == 0x14001d7a);
371 assert_eq!(old_time.is_some(), new_time.is_some());392 if object.jcid == 0x6000d {
393 assert!(new_time.is_some());
394 } else {
395 assert_eq!(old_time.is_some(), new_time.is_some());
396 }
372 if let Some(time) = new_time {397 if let Some(time) = new_time {
373 assert_eq!(&time.value, modified);398 assert_eq!(&time.value, modified);
374 }399 }
...@@ -413,10 +438,10 @@ fn text_splices_preserve_formats_objects_and_history_in_one_transaction() {...@@ -413,10 +438,10 @@ fn text_splices_preserve_formats_objects_and_history_in_one_transaction() {
413 index.spaces[space_id].labels[&(ExGuid::default(), 1)],438 index.spaces[space_id].labels[&(ExGuid::default(), 1)],
414 )439 )
415 .unwrap();440 .unwrap();
416 assert_eq!(previous.objects.len(), current.objects.len());
417 if *space_id == sid {441 if *space_id == sid {
418 assert_other_objects_preserved(&previous, &current, oid);442 assert_other_objects_preserved(&previous, &current, oid);
419 } else {443 } else {
444 assert_eq!(previous.objects.len(), current.objects.len());
420 for (id, object) in previous.objects {445 for (id, object) in previous.objects {
421 assert_eq!(object.data, current.objects[&id].data);446 assert_eq!(object.data, current.objects[&id].data);
422 }447 }
crates/onestore/tests/insertion.rs+60-1
...@@ -4,6 +4,8 @@ mod checkpoint;...@@ -4,6 +4,8 @@ mod checkpoint;
4mod current;4mod current;
5#[path = "support/disk.rs"]5#[path = "support/disk.rs"]
6mod disk;6mod disk;
7#[path = "support/metadata.rs"]
8mod metadata;
7#[path = "support/ops.rs"]9#[path = "support/ops.rs"]
8mod ops;10mod ops;
9#[path = "support/sweep.rs"]11#[path = "support/sweep.rs"]
...@@ -415,6 +417,60 @@ fn paragraph_and_outline_insertions_publish_metadata_and_references_together() {...@@ -415,6 +417,60 @@ fn paragraph_and_outline_insertions_publish_metadata_and_references_together() {
415 }417 }
416}418}
417419
420#[test]
421fn first_revision_metadata_checkpoints_once_and_keeps_history() {
422 let source = onestore::create_section("metadata.one", "Original", "Same author").unwrap();
423 let (sid, _, _, _, text) = targets(&source);
424 let store = Store::parse(&source).unwrap();
425 let index = RevisionIndex::parse(&store).unwrap();
426 let original = index.resolve_active(sid).unwrap();
427 assert!(!original.roots.contains_key(&4));
428 let arena = onestore::Arena::default();
429 let mut section = onestore::Section::open(&arena, source.clone()).unwrap();
430 let at = 134_000_000_000_012_345;
431 let mut previous = None;
432 for at in [at, at + 1] {
433 section
434 .apply(
435 "Same author",
436 &Edit {
437 at,
438 ops: vec![Op::Page {
439 space: sid,
440 op: PageOp::Text {
441 text,
442 range: 0..0,
443 with: "x".into(),
444 },
445 }],
446 },
447 )
448 .unwrap();
449 section.seal().unwrap().unwrap();
450 let image = section.image();
451 let current_store = Store::parse(&image).unwrap();
452 let current = RevisionIndex::parse(&current_store).unwrap();
453 current.validate_current().unwrap();
454 let rid = current.active(sid).unwrap();
455 assert_eq!(current.spaces[&sid].revisions[&rid].dependency, previous);
456 let revision = current.resolve_active(sid).unwrap();
457 assert_eq!(metadata::check(&original, &revision, "Same author"), at);
458 assert!(
459 revision
460 .objects
461 .keys()
462 .all(|id| original.objects.contains_key(id) || *id == revision.roots[&4])
463 );
464 for old in index.spaces[&sid].revisions.keys() {
465 assert_eq!(
466 format!("{:?}", index.resolve(sid, *old).unwrap()),
467 format!("{:?}", current.resolve(sid, *old).unwrap())
468 );
469 }
470 previous = Some(rid);
471 }
472}
473
418#[test]474#[test]
419fn repeated_insertions_and_formatting_share_immutable_objects() {475fn repeated_insertions_and_formatting_share_immutable_objects() {
420 let mut source = onestore::create_section("shared.one", "Original", "Same author").unwrap();476 let mut source = onestore::create_section("shared.one", "Original", "Same author").unwrap();
...@@ -468,7 +524,10 @@ fn repeated_insertions_and_formatting_share_immutable_objects() {...@@ -468,7 +524,10 @@ fn repeated_insertions_and_formatting_share_immutable_objects() {
468 counts.push((object.jcid, object.reference_count));524 counts.push((object.jcid, object.reference_count));
469 }525 }
470 counts.sort();526 counts.sort();
471 assert_eq!(counts, [(0x120001, 26), (0x12004d, 12), (0x12004d, 25)]);527 metadata::check(&raw, &raw, "Same author");
528 let author = metadata::author(&raw.objects[&raw.roots[&4]]);
529 assert_eq!(raw.objects[&author].jcid, 0x120001);
530 assert_eq!(counts, [(0x120001, 27), (0x12004d, 12), (0x12004d, 25)]);
472 let document = Document::parse(&index).unwrap();531 let document = Document::parse(&index).unwrap();
473 let space = &document.spaces[&sid];532 let space = &document.spaces[&sid];
474 let view = &space.revisions[&space.contexts[&ExGuid::default()]];533 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
crates/onestore/tests/page_movement.rs+56-14
...@@ -11,6 +11,8 @@ use onestore::{...@@ -11,6 +11,8 @@ use onestore::{
11mod current;11mod current;
12#[path = "support/disk.rs"]12#[path = "support/disk.rs"]
13mod disk;13mod disk;
14#[path = "support/metadata.rs"]
15mod metadata;
14#[path = "support/sweep.rs"]16#[path = "support/sweep.rs"]
15mod sweep;17mod sweep;
1618
...@@ -91,14 +93,33 @@ fn verify(before: &[u8], after: &[u8], native: &[u8]) {...@@ -91,14 +93,33 @@ fn verify(before: &[u8], after: &[u8], native: &[u8]) {
91 let new = indexes[1]93 let new = indexes[1]
92 .resolve(*sid, indexes[1].spaces[sid].labels[&(ExGuid::default(), 1)])94 .resolve(*sid, indexes[1].spaces[sid].labels[&(ExGuid::default(), 1)])
93 .unwrap();95 .unwrap();
94 assert_eq!(old.roots, new.roots);96 let changed = old.objects[&old.roots[&2]].data != new.objects[&new.roots[&2]].data;
95 assert_eq!(97 let mut authors = std::collections::BTreeSet::new();
96 old.objects.keys().collect::<Vec<_>>(),98 if changed {
97 new.objects.keys().collect::<Vec<_>>()99 metadata::check(&old, &new, "Author");
98 );100 authors.insert(metadata::author(&new.objects[&new.roots[&4]]));
101 if let Some(id) = old.roots.get(&4) {
102 authors.extend(old.objects[id].references().unwrap().objects);
103 }
104 assert!(new.objects.keys().all(|id| {
105 old.objects.contains_key(id) || *id == new.roots[&4] || authors.contains(id)
106 }));
107 } else {
108 assert_eq!(old.roots, new.roots);
109 assert_eq!(
110 old.objects.keys().collect::<Vec<_>>(),
111 new.objects.keys().collect::<Vec<_>>()
112 );
113 }
99 for (id, object) in &old.objects {114 for (id, object) in &old.objects {
100 if *id != old.roots[&2] {115 if *id != old.roots[&2] && !(changed && Some(id) == old.roots.get(&4)) {
101 assert_eq!(format!("{object:?}"), format!("{:?}", new.objects[id]));116 let current = &new.objects[id];
117 assert_eq!(object.data, current.data);
118 assert_eq!(object.jcid, current.jcid);
119 assert_eq!(object.global_ids, current.global_ids);
120 if !authors.contains(id) {
121 assert_eq!(object.reference_count, current.reference_count);
122 }
102 }123 }
103 }124 }
104 }125 }
...@@ -383,13 +404,34 @@ fn metadata_copy_order_does_not_change_page_identity() {...@@ -383,13 +404,34 @@ fn metadata_copy_order_does_not_change_page_identity() {
383 let stores = [edited.as_bytes(), expected.as_slice()].map(|b| Store::parse(b).unwrap());404 let stores = [edited.as_bytes(), expected.as_slice()].map(|b| Store::parse(b).unwrap());
384 let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap());405 let indexes = stores.each_ref().map(|s| RevisionIndex::parse(s).unwrap());
385 let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap());406 let documents = indexes.each_ref().map(|i| Document::parse(i).unwrap());
386 for sid in documents[0].spaces.keys() {407 for space_id in documents[0].spaces.keys() {
387 let nodes = documents.each_ref().map(|d| {408 let views = documents
388 let space = &d.spaces[sid];409 .each_ref()
389 serde_json::to_value(&space.revisions[&space.contexts[&ExGuid::default()]].nodes)410 .map(|d| d.spaces[space_id].active().unwrap());
390 .unwrap()411 let mut nodes = views.map(|view| view.nodes.clone());
391 });412 if *space_id == sid {
392 assert_eq!(nodes[0], nodes[1], "{sid}");413 let raw = indexes.each_ref().map(|index| {
414 index
415 .resolve(
416 *space_id,
417 index.spaces[space_id].labels[&(ExGuid::default(), 1)],
418 )
419 .unwrap()
420 });
421 metadata::check(&raw[1], &raw[0], "Author");
422 let author = metadata::author(&raw[0].objects[&raw[0].roots[&4]]);
423 if !nodes[1].contains_key(&author) {
424 nodes[0].remove(&author).unwrap();
425 }
426 for (nodes, view) in nodes.iter_mut().zip(views) {
427 nodes.remove(&view.roots[&4]).unwrap();
428 }
429 }
430 assert_eq!(
431 serde_json::to_value(&nodes[0]).unwrap(),
432 serde_json::to_value(&nodes[1]).unwrap(),
433 "{space_id}"
434 );
393 }435 }
394}436}
395437
crates/onestore/tests/paragraph.rs+28
...@@ -12,6 +12,8 @@ mod checkpoint;...@@ -12,6 +12,8 @@ mod checkpoint;
12mod current;12mod current;
13#[path = "support/disk.rs"]13#[path = "support/disk.rs"]
14mod disk;14mod disk;
15#[path = "support/metadata.rs"]
16mod metadata;
15#[path = "support/ops.rs"]17#[path = "support/ops.rs"]
16mod ops;18mod ops;
1719
...@@ -220,6 +222,22 @@ fn joins_match_native_graphs_tags_and_inherited_character_styles() {...@@ -220,6 +222,22 @@ fn joins_match_native_graphs_tags_and_inherited_character_styles() {
220 .unwrap();222 .unwrap();
221 let current_rid = current_document.spaces[sid].contexts[&ExGuid::default()];223 let current_rid = current_document.spaces[sid].contexts[&ExGuid::default()];
222 let new_raw = current_index.resolve(*sid, current_rid).unwrap();224 let new_raw = current_index.resolve(*sid, current_rid).unwrap();
225 let modified = metadata::check(&old_raw, &new_raw, "Author");
226 let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]);
227 changed.extend(old_raw.roots.get(&4));
228 for id in &changed {
229 if current
230 .nodes
231 .get(id)
232 .is_some_and(|node| matches!(node.kind, Kind::Paragraph { .. }))
233 {
234 assert_eq!(metadata::author(&new_raw.objects[id]), author);
235 assert_eq!(
236 current.nodes[id].modified.map(u64::from),
237 Some(modified / 10_000_000 - 11_960_006_400)
238 );
239 }
240 }
223 for (id, object) in old_raw.objects {241 for (id, object) in old_raw.objects {
224 if !changed.contains(&id) {242 if !changed.contains(&id) {
225 assert_eq!(243 assert_eq!(
...@@ -409,6 +427,16 @@ fn splits_partition_native_paragraphs_at_every_scalar_boundary() {...@@ -409,6 +427,16 @@ fn splits_partition_native_paragraphs_at_every_scalar_boundary() {
409 let new_raw = current_index427 let new_raw = current_index
410 .resolve(*sid, current_space.contexts[&ExGuid::default()])428 .resolve(*sid, current_space.contexts[&ExGuid::default()])
411 .unwrap();429 .unwrap();
430 let modified = metadata::check(&old_raw, &new_raw, "Author");
431 let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]);
432 changed.extend(old_raw.roots.get(&4));
433 for id in [paragraph, new_paragraph] {
434 assert_eq!(metadata::author(&new_raw.objects[&id]), author);
435 assert_eq!(
436 after.nodes[&id].modified.map(u64::from),
437 Some(modified / 10_000_000 - 11_960_006_400)
438 );
439 }
412 for (id, object) in old_raw.objects {440 for (id, object) in old_raw.objects {
413 if !changed.contains(&id) {441 if !changed.contains(&id) {
414 assert_eq!(object.data, new_raw.objects[&id].data, "untouched {id}");442 assert_eq!(object.data, new_raw.objects[&id].data, "untouched {id}");
crates/onestore/tests/support/checkpoint.rs+5-1
...@@ -26,7 +26,11 @@ pub fn pending(source: &[u8], space: ExGuid, text: ExGuid) -> Vec<u8> {...@@ -26,7 +26,11 @@ pub fn pending(source: &[u8], space: ExGuid, text: ExGuid) -> Vec<u8> {
26 with: with.into(),26 with: with.into(),
27 },27 },
28 };28 };
29 for at in 0..512 - depth(source) as u64 {29 for at in 0..=512 {
30 let image = section.image();
31 if depth(&image) == 512 {
32 return image;
33 }
30 let ops = vec![retouch(0..0, "x"), retouch(0..1, "")];34 let ops = vec![retouch(0..0, "x"), retouch(0..1, "")];
31 let at = 134_000_000_000_000_000 + at * 10_000_000;35 let at = 134_000_000_000_000_000 + at * 10_000_000;
32 section.apply("Author", &Edit { at, ops }).unwrap();36 section.apply("Author", &Edit { at, ops }).unwrap();
crates/onestore/tests/support/metadata.rs created+119
...@@ -0,0 +1,119 @@
1use onestore::{ExGuid, IdStream, Object, ObjectData, PropertySets, ResolvedRevision, Value};
2
3pub fn author(object: &Object<'_>) -> ExGuid {
4 let ObjectData::Properties(bytes) = object.data else {
5 panic!("AuthorMostRecent needs a property object")
6 };
7 let fields = PropertySets::parse(bytes).unwrap();
8 let Value::References {
9 stream: IdStream::Objects,
10 compact_ids,
11 } = fields.sets[0]
12 .iter()
13 .find(|field| field.id == 0x20001d79)
14 .unwrap()
15 .value
16 else {
17 panic!("AuthorMostRecent needs one object reference")
18 };
19 let compact = u32::from_le_bytes(compact_ids.try_into().unwrap());
20 ExGuid {
21 guid: object.global_ids[&(compact >> 8)],
22 n: compact & 255,
23 }
24}
25
26/// Checks the revision context's author/time while preserving every other metadata field.
27pub fn check(before: &ResolvedRevision<'_>, after: &ResolvedRevision<'_>, name: &str) -> u64 {
28 let roots = |revision: &ResolvedRevision<'_>| {
29 revision
30 .roots
31 .iter()
32 .filter(|(kind, _)| **kind != 4)
33 .map(|(kind, id)| (*kind, *id))
34 .collect::<Vec<_>>()
35 };
36 assert_eq!(roots(before), roots(after));
37 let id = after.roots[&4];
38 let metadata = &after.objects[&id];
39 assert_eq!(metadata.jcid, 0x20044);
40 let ObjectData::Properties(bytes) = metadata.data else {
41 panic!()
42 };
43 let fields = PropertySets::parse(bytes).unwrap();
44 let Value::Bytes(time) = fields.sets[0]
45 .iter()
46 .find(|field| field.id == 0x18001d77)
47 .unwrap()
48 .value
49 else {
50 panic!()
51 };
52 let filetime = u64::from_le_bytes(time.try_into().unwrap());
53 assert!(filetime >= 119_600_064_000_000_000);
54 let author_id = author(metadata);
55 let named = &after.objects[&author_id];
56 assert_eq!(named.jcid, 0x120001);
57 let ObjectData::Properties(bytes) = named.data else {
58 panic!()
59 };
60 let named_fields = PropertySets::parse(bytes).unwrap();
61 let Value::Bytes(stored) = named_fields.sets[0]
62 .iter()
63 .find(|field| field.id == 0x1c001d75)
64 .unwrap()
65 .value
66 else {
67 panic!()
68 };
69 let expected: Vec<_> = name
70 .encode_utf16()
71 .chain([0])
72 .flat_map(u16::to_le_bytes)
73 .collect();
74 assert_eq!(stored, expected);
75 if let Some(old) = before.objects.get(&author_id) {
76 assert_eq!(old.data, named.data);
77 }
78 if let Some(old_id) = before.roots.get(&4) {
79 assert_eq!(*old_id, id);
80 let old = &before.objects[old_id];
81 assert_eq!(old.jcid, metadata.jcid);
82 assert!(metadata.global_ids.iter().all(|(entry, guid)| {
83 old.global_ids.get(entry) == Some(guid) || *guid == author_id.guid
84 }));
85 let ObjectData::Properties(old_bytes) = old.data else {
86 panic!()
87 };
88 let mut old_fields = PropertySets::parse(old_bytes).unwrap();
89 let mut new = fields;
90 for fields in [&mut old_fields, &mut new] {
91 fields.sets[0].retain(|field| ![0x18001d77, 0x20001d79].contains(&field.id));
92 }
93 assert_eq!(old_fields.sets, new.sets);
94 for field in old_fields.sets.iter().flatten() {
95 if let Value::References { compact_ids, .. } = field.value {
96 let ids = compact_ids.chunks_exact(4);
97 assert!(ids.remainder().is_empty());
98 for compact in ids {
99 let entry = u32::from_le_bytes(compact.try_into().unwrap()) >> 8;
100 assert_eq!(
101 metadata.global_ids.get(&entry),
102 Some(&old.global_ids[&entry])
103 );
104 }
105 }
106 }
107 } else {
108 assert!(!before.objects.contains_key(&id));
109 assert_eq!(fields.sets.len(), 1);
110 assert_eq!(
111 fields.sets[0]
112 .iter()
113 .map(|field| field.id)
114 .collect::<Vec<_>>(),
115 [0x18001d77, 0x20001d79]
116 );
117 }
118 filetime
119}