1//! Earlier states of a section: a page as one of its stored revisions holds it, the section
2//! as some revisions of its spaces leave it, and revisions sealed under chosen names.
3
4use onestore::{
5 Arena, ExGuid, PageCreation, RevisionIndex, Section, Store,
6 op::{Edit, Op, PageOp, SectionOp},
7};
8use std::collections::BTreeMap;
9
10fn read(path: &str) -> Vec<u8> {
11 std::fs::read(format!(
12 "{}/../../corpus/{path}",
13 env!("CARGO_MANIFEST_DIR")
14 ))
15 .unwrap()
16}
17
18/// The active revision of each object space.
19fn active(image: &[u8]) -> BTreeMap<ExGuid, ExGuid> {
20 let store = Store::parse(image).unwrap();
21 let index = RevisionIndex::parse(&store).unwrap();
22 index
23 .spaces
24 .keys()
25 .filter_map(|space| Some((*space, index.active(*space).ok()?)))
26 .collect()
27}
28
29/// Two OneNote 2010 clients' captures of one notebook, earliest first: each later file holds
30/// every revision the earlier ones made current.
31const STAGES: [&[&str]; 3] = [
32 &[
33 "conflict-page/native/initial/notebook/synthetic.one",
34 "conflict-page/native/b-published/notebook/synthetic.one",
35 "conflict-page/native/conflict/notebook/synthetic.one",
36 ],
37 &[
38 "conflict-page/native-pages/initial/notebook/synthetic.one",
39 "conflict-page/native-pages/b-published/notebook/synthetic.one",
40 "conflict-page/native-pages/merged/notebook/synthetic.one",
41 ],
42 &[
43 "conflict-page/native-restore/initial/notebook/synthetic.one",
44 "conflict-page/native-restore/b-published/notebook/synthetic.one",
45 "conflict-page/native-restore/merged/notebook/synthetic.one",
46 ],
47];
48
49#[test]
50fn each_stored_revision_of_a_page_reads_as_onenote_showed_it_then() {
51 let mut compared = 0;
52 for stages in STAGES {
53 let last = read(stages[stages.len() - 1]);
54 let arena = Arena::default();
55 let history = Section::open(&arena, last.clone()).unwrap();
56 for earlier in &stages[..stages.len() - 1] {
57 let earlier = read(earlier);
58 let arena = Arena::default();
59 let mut then = Section::open(&arena, earlier.clone()).unwrap();
60 let revisions = active(&earlier);
61 for (space, ..) in then.pages().unwrap() {
62 assert_eq!(
63 history.page_at(space, revisions[&space]).unwrap(),
64 then.page(space).unwrap(),
65 "{space} at {}",
66 revisions[&space]
67 );
68 compared += 1;
69 }
70 // The section as those revisions leave it is the earlier file's.
71 let arena = Arena::default();
72 let mut state = Section::open_at(&arena, vec![last.clone()], &revisions).unwrap();
73 assert_eq!(state.pages().unwrap(), then.pages().unwrap());
74 assert_eq!(state.series().unwrap(), then.series().unwrap());
75 for (space, ..) in then.pages().unwrap() {
76 assert_eq!(state.page(space).unwrap(), then.page(space).unwrap());
77 }
78 }
79 }
80 assert!(compared >= 10, "{compared}");
81}
82
83#[test]
84fn an_earlier_state_needs_its_page_list() {
85 let image = read("conflict-page/native/conflict/notebook/synthetic.one");
86 let mut revisions = active(&image);
87 let arena = Arena::default();
88 let root = Section::open(&arena, image.clone()).unwrap().root();
89 revisions.remove(&root);
90 assert!(Section::open_at(&Arena::default(), vec![image], &revisions).is_err());
91}
92
93#[test]
94fn a_sealed_revision_takes_the_name_given_it() {
95 let image = read("conflict-page/native/initial/notebook/synthetic.one");
96 let arena = Arena::default();
97 let mut section = Section::open(&arena, image).unwrap();
98 let (space, ..) = section.pages().unwrap()[0].clone();
99 let body = section
100 .page(space)
101 .unwrap()
102 .objects
103 .iter()
104 .find_map(|object| match object {
105 onestore::page::PageObject::Outline(outline) => outline
106 .paragraphs
107 .iter()
108 .find_map(|paragraph| paragraph.text().map(|text| text.id)),
109 _ => None,
110 })
111 .unwrap();
112 let current = section.revisions().find(|(id, _)| *id == space).unwrap().1;
113 let type_body = |section: &mut Section<'_>| {
114 section
115 .apply(
116 "Author",
117 &Edit {
118 at: 134_030_000_000_000_000,
119 ops: vec![Op::Page {
120 space,
121 op: PageOp::Text {
122 text: body,
123 range: 0..0,
124 with: "Named ".into(),
125 },
126 }],
127 },
128 )
129 .unwrap()
130 };
131 type_body(&mut section);
132 assert!(
133 section
134 .seal_as(&BTreeMap::from([(space, current)]))
135 .is_err()
136 );
137 let arena = Arena::default();
138 let image = read("conflict-page/native/initial/notebook/synthetic.one");
139 let mut section = Section::open(&arena, image).unwrap();
140 type_body(&mut section);
141 let creation = PageCreation::new(None, Some("Also named"), "Author")
142 .unwrap()
143 .in_space([7; 16])
144 .unwrap();
145 let created = creation.space();
146 section
147 .apply(
148 "Author",
149 &Edit {
150 at: 134_030_000_000_000_000,
151 ops: vec![Op::Section(SectionOp::Create(creation))],
152 },
153 )
154 .unwrap();
155 let name = |n| ExGuid {
156 guid: [n; 16],
157 n: 1,
158 };
159 section
160 .seal_as(&BTreeMap::from([(space, name(1)), (created, name(2))]))
161 .unwrap()
162 .unwrap();
163 let sealed = section.image();
164 let revisions = active(&sealed);
165 assert_eq!(revisions[&space], name(1));
166 assert_eq!(revisions[&created], name(2));
167 assert_eq!(
168 created,
169 ExGuid {
170 guid: [7; 16],
171 n: 1
172 }
173 );
174 // The earlier revision still reads as it was.
175 let arena = Arena::default();
176 let reopened = Section::open(&arena, sealed).unwrap();
177 let typed =
178 |page: onestore::page::Page| serde_json::to_string(&page).unwrap().contains("Named ");
179 assert!(!typed(reopened.page_at(space, current).unwrap()));
180 assert!(typed(reopened.page(space).unwrap()));
181}