| author | |
| committer | |
| log | e9d0ff1435aa931d38788bf676968c992e370104 |
| tree | deba8d70fb04411dcddb63f32d46d6253cc8f31a |
| parent | de3ecb8964829cbf26e3abd78486bc59405b2ac4 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
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-sol7 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 | )); | ||
| 2786 | 2798 | ||
| 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 @@ |
| 2 | mod checkpoint; | 2 | mod checkpoint; |
| 3 | #[path = "support/disk.rs"] | 3 | #[path = "support/disk.rs"] |
| 4 | mod disk; | 4 | mod disk; |
| 5 | #[path = "support/metadata.rs"] | ||
| 6 | mod metadata; | ||
| 5 | #[path = "support/ops.rs"] | 7 | #[path = "support/ops.rs"] |
| 6 | mod ops; | 8 | mod 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 | current | 350 | current |
| 335 | .global_ids | 351 | .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; |
| 4 | mod current; | 4 | mod current; |
| 5 | #[path = "support/disk.rs"] | 5 | #[path = "support/disk.rs"] |
| 6 | mod disk; | 6 | mod disk; |
| 7 | #[path = "support/metadata.rs"] | ||
| 8 | mod metadata; | ||
| 7 | #[path = "support/ops.rs"] | 9 | #[path = "support/ops.rs"] |
| 8 | mod ops; | 10 | mod 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 | } |
| 417 | 419 | ||
| 420 | #[test] | ||
| 421 | fn 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] |
| 419 | fn repeated_insertions_and_formatting_share_immutable_objects() { | 475 | fn 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::{ |
| 11 | mod current; | 11 | mod current; |
| 12 | #[path = "support/disk.rs"] | 12 | #[path = "support/disk.rs"] |
| 13 | mod disk; | 13 | mod disk; |
| 14 | #[path = "support/metadata.rs"] | ||
| 15 | mod metadata; | ||
| 14 | #[path = "support/sweep.rs"] | 16 | #[path = "support/sweep.rs"] |
| 15 | mod sweep; | 17 | mod sweep; |
| 16 | 18 | ||
| ... | @@ -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 | } |
| 395 | 437 |
crates/onestore/tests/paragraph.rs+28| ... | @@ -12,6 +12,8 @@ mod checkpoint; | ... | @@ -12,6 +12,8 @@ mod checkpoint; |
| 12 | mod current; | 12 | mod current; |
| 13 | #[path = "support/disk.rs"] | 13 | #[path = "support/disk.rs"] |
| 14 | mod disk; | 14 | mod disk; |
| 15 | #[path = "support/metadata.rs"] | ||
| 16 | mod metadata; | ||
| 15 | #[path = "support/ops.rs"] | 17 | #[path = "support/ops.rs"] |
| 16 | mod ops; | 18 | mod ops; |
| 17 | 19 | ||
| ... | @@ -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_index | 427 | 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 @@ | ||
| 1 | use onestore::{ExGuid, IdStream, Object, ObjectData, PropertySets, ResolvedRevision, Value}; | ||
| 2 | |||
| 3 | pub 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. | ||
| 27 | pub 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 | } | ||