diff --git a/crates/onestore/tests/outline.rs b/crates/onestore/tests/outline.rs index 0465495fa25646184eae83ef951a62d33e35c4fc..090ad79575b62bb00504d0fa9899d4e79338c23f 100644 --- a/crates/onestore/tests/outline.rs +++ b/crates/onestore/tests/outline.rs @@ -9,6 +9,8 @@ use std::collections::BTreeSet; mod current; #[path = "support/disk.rs"] mod disk; +#[path = "support/metadata.rs"] +mod metadata; #[path = "support/ops.rs"] mod ops; #[path = "support/sweep.rs"] @@ -67,6 +69,7 @@ fn geometry_and_expansion_preserve_unrelated_properties_objects_and_history() { for (name, sid, object, edit) in cases(&document) { let restored = serde_json::from_value(serde_json::to_value(edit).unwrap()).unwrap(); assert_eq!(edit, restored); + let started = ops::now(); let prepared = ops::page_op( SOURCE, sid, @@ -76,6 +79,7 @@ fn geometry_and_expansion_preserve_unrelated_properties_objects_and_history() { }, ) .unwrap(); + let finished = ops::now(); let after_store = Store::parse(prepared.as_bytes()).unwrap(); assert!(after_store.checksum_mismatches.is_empty()); let after_index = RevisionIndex::parse(&after_store).unwrap(); @@ -100,15 +104,33 @@ fn geometry_and_expansion_preserve_unrelated_properties_objects_and_history() { .then_some(*parent) })); } - assert_eq!( - before.nodes.keys().collect::>(), - after.nodes.keys().collect::>() + let old_raw = index.resolve_active(sid).unwrap(); + let new_raw = after_index.resolve_active(sid).unwrap(); + let at = metadata::check(&old_raw, &new_raw, "Author"); + assert!((started..=finished).contains(&at)); + let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]); + assert!( + after + .nodes + .keys() + .all(|id| before.nodes.contains_key(id) || *id == after.roots[&4] || *id == author) ); for (id, node) in &before.nodes { + if Some(id) == before.roots.get(&4) { + continue; + } let mut expected = serde_json::to_value(node).unwrap(); let actual = serde_json::to_value(&after.nodes[id]).unwrap(); if changed.contains(id) { expected["modified"] = actual["modified"].clone(); + if matches!(node.kind, Kind::Paragraph { .. }) { + assert_eq!(after.nodes[id].latest_author, Some(author)); + assert_eq!( + after.nodes[id].modified.map(u64::from), + Some(at / 10_000_000 - 11_960_006_400) + ); + expected["latest_author"] = actual["latest_author"].clone(); + } } if *id == object { match edit { @@ -344,6 +366,7 @@ fn outline_movement_updates_both_automatic_title_fields_without_changing_content (144.0, 36.0, "First"), (0.0, 144.0, "First"), ] { + let started = ops::now(); let edit = ops::page_op( &source, sid, @@ -353,12 +376,17 @@ fn outline_movement_updates_both_automatic_title_fields_without_changing_content }, ) .unwrap(); + let finished = ops::now(); let saved = Store::parse(edit.as_bytes()).unwrap(); let saved_index = RevisionIndex::parse(&saved).unwrap(); saved_index.validate_current().unwrap(); let saved_document = Document::parse(&saved_index).unwrap(); let saved_space = &saved_document.spaces[&sid]; let after = &saved_space.revisions[&saved_space.contexts[&ExGuid::default()]]; + let old_raw = index.resolve_active(sid).unwrap(); + let new_raw = saved_index.resolve_active(sid).unwrap(); + let at = metadata::check(&old_raw, &new_raw, "Author"); + assert!((started..=finished).contains(&at)); let metadata = before.roots[&2]; assert!( matches!(&after.nodes[&metadata].kind, Kind::Metadata { title: Some(title), .. } if title == expected) @@ -371,7 +399,7 @@ fn outline_movement_updates_both_automatic_title_fields_without_changing_content after.nodes.keys().collect::>() ); for (oid, node) in &before.nodes { - if ![page, metadata, insertion].contains(oid) { + if ![page, metadata, insertion, before.roots[&4]].contains(oid) { assert_eq!( serde_json::to_value(node).unwrap(), serde_json::to_value(&after.nodes[oid]).unwrap() diff --git a/crates/onestore/tests/page_writer.rs b/crates/onestore/tests/page_writer.rs index 0903175360eb0c8ec361012f840f569613869e9d..b6b8e2f8dd0e753f673e7b8d5d266bdbc3cc2b7b 100644 --- a/crates/onestore/tests/page_writer.rs +++ b/crates/onestore/tests/page_writer.rs @@ -1,6 +1,8 @@ //! A page edit lowered whole (`op::lower_page`) must publish the same graph its ops applied //! one by one publish, in one transaction, and must round-trip the model it was given. +#[path = "support/metadata.rs"] +mod metadata; #[path = "support/ops.rs"] mod ops; @@ -164,7 +166,7 @@ fn same_element(revision_a: &Revision<'_>, revision_b: &Revision<'_>, id: ExGuid } /// Every active revision the ops produced one by one is reproduced by the page lowered whole, -/// ignoring writer-allocated immutable styles and author records. Modification times are +/// ignoring writer-allocated immutable styles and author records. Paragraph modification times are /// not compared: a reorder can be attributed to either sibling, and the model cannot say which. fn equivalent(reference: &[u8], written: &[u8]) { let store_a = Store::parse(reference).unwrap(); @@ -201,11 +203,29 @@ fn equivalent(reference: &[u8], written: &[u8]) { } } -/// Lowers the page the ops produced whole and checks both contracts. +/// Both lowerings use the last op's FILETIME so revision metadata compares exactly. fn oracle(source: &[u8], space: ExGuid, reference: &[u8]) -> Vec { let after = page_in(reference, space); - let prepared = ops::saved(source, space, &after).unwrap(); - let written = prepared.as_slice().to_vec(); + let store = Store::parse(reference).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let revision = index.resolve_active(space).unwrap(); + let source_store = Store::parse(source).unwrap(); + let source_index = RevisionIndex::parse(&source_store).unwrap(); + let original = source_index.resolve_active(space).unwrap(); + let at = metadata::check(&original, &revision, AUTHOR); + let arena = onestore::Arena::default(); + let mut section = onestore::Section::open(&arena, source.to_vec()).unwrap(); + let before = section.page(space).unwrap(); + let ops = onestore::op::lower_page(&before, &after) + .unwrap() + .into_iter() + .map(|op| onestore::op::Op::Page { space, op }) + .collect(); + section + .apply(AUTHOR, &onestore::op::Edit { at, ops }) + .unwrap(); + section.seal().unwrap().unwrap(); + let written = section.image(); assert_eq!(page_in(&written, space), after, "model round trip"); equivalent(reference, &written); let transactions = |bytes: &[u8]| Store::parse(bytes).unwrap().header.transaction_count; @@ -583,7 +603,9 @@ fn model_edits_round_trip_and_leave_other_objects_untouched() { ); outline.paragraphs.push(paragraph); after.objects.push(PageObject::Outline(outline)); + let started = ops::now(); let written = ops::saved(TREES, space, &after).unwrap(); + let finished = ops::now(); let stored = page_in(written.as_slice(), space); let normalize = |page: &Page| -> Vec<(ExGuid, Vec<(ExGuid, String)>)> { body(page) @@ -616,6 +638,8 @@ fn model_edits_round_trip_and_leave_other_objects_untouched() { let store = Store::parse(written.as_slice()).unwrap(); let index = RevisionIndex::parse(&store).unwrap(); let raw_after = index.resolve_active(space).unwrap(); + let at = metadata::check(&raw_before, &raw_after, AUTHOR); + assert!((started..=finished).contains(&at)); let touched: BTreeSet = body(&after) .iter() .flat_map(|o| { @@ -629,7 +653,10 @@ fn model_edits_round_trip_and_leave_other_objects_untouched() { .collect(); let mut untouched = 0; for (id, object) in &raw_before.objects { - if touched.contains(id) || matches!(object.jcid, 0x12004d | 0x120001) { + if touched.contains(id) + || Some(id) == raw_before.roots.get(&4) + || matches!(object.jcid, 0x12004d | 0x120001) + { continue; } let after = &raw_after.objects[id]; diff --git a/crates/onestore/tests/tree.rs b/crates/onestore/tests/tree.rs index 72e44ae7b1870a0cf63cf61dd07bea66bc512932..0969dfff67dd726624d1a7a2559bba8cb04d5324 100644 --- a/crates/onestore/tests/tree.rs +++ b/crates/onestore/tests/tree.rs @@ -9,6 +9,8 @@ use std::collections::{BTreeMap, BTreeSet}; mod current; #[path = "support/disk.rs"] mod disk; +#[path = "support/metadata.rs"] +mod metadata; #[path = "support/ops.rs"] mod ops; #[path = "support/sweep.rs"] @@ -191,8 +193,10 @@ fn native_subtree_controls_match_with_preserved_fields_and_history() { .into_iter() .map(|op| Op::Page { space: sid, op }) .collect(); + let started = ops::now(); let prepared = ops::apply(source, "Tree author", edit.clone()) .unwrap_or_else(|error| panic!("{name}: {error}")); + let finished = ops::now(); let after_store = Store::parse(prepared.as_bytes()).unwrap(); assert!(after_store.checksum_mismatches.is_empty()); let after_index = RevisionIndex::parse(&after_store).unwrap(); @@ -200,6 +204,11 @@ fn native_subtree_controls_match_with_preserved_fields_and_history() { let after_document = Document::parse(&after_index).unwrap(); let space = &after_document.spaces[&sid]; let after = &space.revisions[&space.contexts[&ExGuid::default()]]; + let old_raw = index.resolve_active(sid).unwrap(); + let new_raw = after_index.resolve_active(sid).unwrap(); + let at = metadata::check(&old_raw, &new_raw, "Tree author"); + assert!((started..=finished).contains(&at)); + let author = metadata::author(&new_raw.objects[&new_raw.roots[&4]]); let space = &native_document.spaces[&sid]; let reference = &space.revisions[&space.contexts[&ExGuid::default()]]; let old_ids = active(before, page); @@ -284,15 +293,14 @@ fn native_subtree_controls_match_with_preserved_fields_and_history() { { assert!(node.modified >= old.modified); expected["modified"] = actual["modified"].clone(); - } - if *id == object - && !name.starts_with("Delete") - && matches!(node.kind, Kind::Paragraph { .. }) - { - expected["latest_author"] = actual["latest_author"].clone(); - assert!( - matches!(&after.nodes[&node.latest_author.unwrap()].kind,Kind::Author{name:Some(name)} if name=="Tree author") - ); + if matches!(node.kind, Kind::Paragraph { .. }) { + assert_eq!(node.latest_author, Some(author)); + assert_eq!( + node.modified.map(u64::from), + Some(at / 10_000_000 - 11_960_006_400) + ); + expected["latest_author"] = actual["latest_author"].clone(); + } } for value in [&mut expected, &mut actual] { value["extra"][0] diff --git a/crates/onestore/tests/writer.rs b/crates/onestore/tests/writer.rs index b515b41859e354db80e52b00cd52eb13f01ba21e..55e1eb046610ab934803a76130071590333fb86e 100644 --- a/crates/onestore/tests/writer.rs +++ b/crates/onestore/tests/writer.rs @@ -1,3 +1,5 @@ +#[path = "support/metadata.rs"] +mod metadata; #[path = "support/ops.rs"] mod ops; @@ -390,6 +392,7 @@ fn checkpoints_bound_dependencies_and_preserve_native_objects_and_history() { let store = Store::parse(&bytes).unwrap(); let before = RevisionIndex::parse(&store).unwrap(); let rid = before.spaces[&sid].labels[&(ExGuid::default(), 1)]; + let at = 134_000_000_000_000_000 + u64::from(operation) * 10_000_000; let written = if section { let typed = |range, with: &str| Op::Page { space: sid, @@ -400,7 +403,6 @@ fn checkpoints_bound_dependencies_and_preserve_native_objects_and_history() { }, }; // Modification times count seconds: each edit is a second later. - let at = 134_000_000_000_000_000 + u64::from(operation) * 10_000_000; let ops = vec![typed(0..0, "x"), typed(0..1, "")]; let arena = onestore::Arena::default(); let mut section = onestore::Section::open(&arena, bytes.clone()).unwrap(); @@ -428,6 +430,9 @@ fn checkpoints_bound_dependencies_and_preserve_native_objects_and_history() { assert_preserved_committed_bytes(&bytes, &written); let old = before.resolve(sid, rid).unwrap(); let current = after.resolve(sid, next).unwrap(); + if section { + assert_eq!(metadata::check(&old, ¤t, "Author"), at); + } assert_eq!(old.roots, current.roots); assert_eq!(old.objects.len(), current.objects.len()); for (id, object) in &old.objects { @@ -456,7 +461,11 @@ fn checkpoints_bound_dependencies_and_preserve_native_objects_and_history() { assert_eq!(a.len(), b.len()); for (a, b) in a.iter().zip(b) { assert_eq!(a.id, b.id); - if a.id != property && !matches!(a.value, Value::References { .. }) + if a.id != property + && !(section + && Some(id) == old.roots.get(&4) + && a.id == 0x18001d77) + && !matches!(a.value, Value::References { .. }) { assert_eq!(a.value, b.value, "{id} {:#x}", a.id); }