1use crate::{
2 ExGuid, RevisionIndex, Store, Transaction,
3 document::{Document, Kind},
4 write::{PropertyObject, RevisionEdit, applied, revisions, write_revisions},
5};
6use std::collections::BTreeMap;
7
8mod disk {
9 use crate as onestore;
10 include!("../../../tests/support/disk.rs");
11}
12
13mod current {
14 use crate as onestore;
15 include!("../../../tests/support/current.rs");
16}
17
18const SOURCE: &[u8] =
19 include_bytes!("../../../../../corpus/page-lifecycle/03-renamed/notebook/Lifecycle.one");
20
21fn nest(source: &[u8]) -> Option<Transaction> {
22 revisions(source, |index| {
23 let document = Document::parse(index)?;
24 let pages = document.pages()?;
25 assert_eq!(pages.len(), 9);
26 let section = index.resolve(
27 index.root,
28 index.spaces[&index.root].labels[&(ExGuid::default(), 1)],
29 )?;
30 let view = &document.spaces[&index.root];
31 let view = view.active().unwrap();
32 let root = section.roots[&1];
33 let parent = *view.nodes[&root]
34 .children
35 .iter()
36 .find(|id| view.nodes[id].spaces.contains(&pages[3].0))
37 .unwrap();
38 let mut series = PropertyObject::from_object(&section.objects[&parent])?;
39 let mut spaces = Vec::new();
40 let mut metadata = Vec::new();
41 let mut replacements = BTreeMap::new();
42 let mut changes = BTreeMap::new();
43 for (ordinal, (sid, page)) in pages[3..6].iter().enumerate() {
44 spaces.extend_from_slice(&series.reference(*sid)?);
45 let raw = index.resolve(*sid, index.spaces[sid].labels[&(ExGuid::default(), 1)])?;
46 let id = raw.roots[&2];
47 let mut object = PropertyObject::from_object(&raw.objects[&id])?;
48 object.set(&[(0x14001dff, &((ordinal + 1) as u32).to_le_bytes())])?;
49 changes.insert(*sid, BTreeMap::from([(id, object)]));
50 let mut copy = PropertyObject::from_object(&raw.objects[&id])?;
51 copy.set(&[(0x14001dff, &((ordinal + 1) as u32).to_le_bytes())])?;
52 let mut copy_id = ExGuid {
53 guid: [
54 0x31, 0xc0, 0xa8, 0x22, 0, 0x36, 0xee, 0x42, 0xb7, 0x14, 0xd7, 0xac, 0xda,
55 0x24, 0x35, 0xe8,
56 ],
57 n: 1,
58 };
59 for (value, salt) in copy_id.guid.iter_mut().zip(page.guid) {
60 *value ^= salt;
61 }
62 copy.reference(copy_id)?;
63 metadata.extend_from_slice(&series.reference(copy_id)?);
64 replacements.insert(copy_id, copy);
65 }
66 let links = PropertyObject {
67 jcid: series.jcid,
68 bytes: crate::create::properties(&[(0x2c001d63, spaces)])?,
69 global_ids: std::sync::Arc::clone(&series.global_ids),
70 };
71 series
72 .copy_property(&links, 0x2c001d63)
73 .expect("copy spaces");
74 series
75 .set(&[(0x24003442, &metadata)])
76 .expect("set metadata links");
77 replacements.insert(parent, series);
78 let mut object = PropertyObject::from_object(&section.objects[&root])?;
79 let mut children = Vec::new();
80 for id in &view.nodes[&root].children {
81 if *id == parent
82 || !view.nodes[id]
83 .spaces
84 .iter()
85 .any(|sid| [pages[4].0, pages[5].0].contains(sid))
86 {
87 children.extend_from_slice(&object.reference(*id)?);
88 }
89 }
90 object.set(&[(0x24001c20, &children)])?;
91 replacements.insert(root, object);
92 changes.insert(index.root, replacements);
93 Ok(changes
94 .into_iter()
95 .map(|(sid, objects)| (sid, RevisionEdit::Update(objects)))
96 .collect())
97 })
98 .unwrap()
99}
100
101#[test]
102fn nesting_publishes_section_order_and_page_levels_in_one_transaction() {
103 let written = applied(SOURCE, nest(SOURCE).as_ref()).unwrap();
104 let before = Store::parse(SOURCE).unwrap();
105 let after = Store::parse(&written).unwrap();
106 assert_eq!(
107 after.header.transaction_count,
108 before.header.transaction_count + 1
109 );
110 assert_eq!(after.header.generation, before.header.generation + 1);
111 let old = RevisionIndex::parse(&before).unwrap();
112 let new = RevisionIndex::parse(&after).unwrap();
113 let document = Document::parse(&new).unwrap();
114 let pages = document.pages().unwrap();
115 assert_eq!(pages, Document::parse(&old).unwrap().pages().unwrap());
116 let mut changed_spaces = 0;
117 for (sid, space) in &old.spaces {
118 changed_spaces += usize::from(space.labels != new.spaces[sid].labels);
119 for rid in space.revisions.keys() {
120 assert_eq!(
121 format!("{:?}", old.resolve(*sid, *rid).unwrap()),
122 format!("{:?}", new.resolve(*sid, *rid).unwrap())
123 );
124 }
125 }
126 assert_eq!(changed_spaces, 3);
127 for (ordinal, (sid, _)) in pages.iter().enumerate() {
128 let view = &document.spaces[sid];
129 let view = view.active().unwrap();
130 let Kind::Metadata { level, .. } = view.nodes[&view.roots[&2]].kind else {
131 panic!()
132 };
133 assert_eq!(
134 level,
135 Some(match ordinal {
136 4 => 2,
137 5 => 3,
138 _ => 1,
139 })
140 );
141 let old_view = old
142 .resolve(*sid, old.spaces[sid].labels[&(ExGuid::default(), 1)])
143 .unwrap();
144 let new_view = new
145 .resolve(*sid, new.spaces[sid].labels[&(ExGuid::default(), 1)])
146 .unwrap();
147 for (id, object) in &old_view.objects {
148 if *id != old_view.roots[&2] {
149 assert_eq!(format!("{object:?}"), format!("{:?}", new_view.objects[id]));
150 }
151 }
152 }
153 let root = &document.spaces[&document.root];
154 let root = root.active().unwrap();
155 assert_eq!(
156 root.nodes[&root.roots[&1]]
157 .children
158 .iter()
159 .map(|id| root.nodes[id].spaces.len())
160 .collect::<Vec<_>>(),
161 [1, 1, 1, 3, 1, 1, 1]
162 );
163 assert_eq!(nest(&written), None);
164 if let Some(output) = std::env::var_os("ONESTORE_PAGE_BATCH_OUTPUT") {
165 std::fs::create_dir(&output).unwrap();
166 std::fs::write(
167 std::path::Path::new(&output).join("Lifecycle.one"),
168 &written,
169 )
170 .unwrap();
171 }
172}
173
174#[test]
175fn interrupted_multi_space_publication_never_exposes_a_partial_nesting() {
176 let transaction = nest(SOURCE).unwrap();
177 let written = applied(SOURCE, Some(&transaction)).unwrap();
178 let old = current::current(SOURCE);
179 let new = current::current(&written);
180 assert_ne!(old, new);
181 for write_limit in [17, 4096] {
182 let mut complete = disk::Disk {
183 visible: SOURCE.to_vec(),
184 durable: SOURCE.to_vec(),
185 operation: 0,
186 fail_at: None,
187 write_limit,
188 random: 1951,
189 };
190 transaction.commit(&mut complete).unwrap();
191 assert_eq!(complete.durable, written);
192 for fail_at in 1..=complete.operation {
193 let mut interrupted = disk::Disk {
194 visible: SOURCE.to_vec(),
195 durable: SOURCE.to_vec(),
196 operation: 0,
197 fail_at: Some(fail_at),
198 write_limit,
199 random: 1951 + fail_at as u64,
200 };
201 transaction.commit(&mut interrupted).unwrap_err();
202 let observed = current::current(&interrupted.durable);
203 assert!(
204 observed == old || observed == new,
205 "{write_limit}:{fail_at}"
206 );
207 }
208 }
209}
210
211#[test]
212fn empty_batches_and_invalid_spaces_do_not_produce_an_edit() {
213 assert_eq!(
214 write_revisions(SOURCE, |_| Ok(BTreeMap::new())).unwrap(),
215 SOURCE
216 );
217 assert!(
218 write_revisions(SOURCE, |_| Ok(BTreeMap::from([(
219 ExGuid::default(),
220 RevisionEdit::Update(BTreeMap::new())
221 )])))
222 .is_err()
223 );
224}
225
226#[test]
227fn repeated_multi_space_edits_cross_counter_carries_and_checkpoint_each_space() {
228 let mut source = crate::create_section("batch.one", "Preserved text", "Author").unwrap();
229 let initial = source.clone();
230 let initial_store = Store::parse(&initial).unwrap();
231 let initial_index = RevisionIndex::parse(&initial_store).unwrap();
232 let mut checkpoints = BTreeMap::new();
233 for step in 1_u32..=514 {
234 let transaction = revisions(&source, |index| {
235 let mut changes = BTreeMap::new();
236 for (sid, space) in &index.spaces {
237 let raw = index.resolve(*sid, space.labels[&(ExGuid::default(), 1)])?;
238 let (id, property) = if *sid == index.root {
239 (raw.roots[&2], 0x14001cbe)
240 } else {
241 (
242 *raw.objects
243 .iter()
244 .find(|(_, object)| object.jcid == 0x6000b)
245 .unwrap()
246 .0,
247 0x14001d7a,
248 )
249 };
250 let mut object = PropertyObject::from_object(&raw.objects[&id])?;
251 object.set(&[(property, &step.to_le_bytes())])?;
252 changes.insert(*sid, BTreeMap::from([(id, object)]));
253 }
254 Ok(changes
255 .into_iter()
256 .map(|(sid, objects)| (sid, RevisionEdit::Update(objects)))
257 .collect())
258 })
259 .unwrap()
260 .unwrap();
261 let written = applied(&source, Some(&transaction)).unwrap();
262 let store = Store::parse(&written).unwrap();
263 assert_eq!(
264 store.header.transaction_count,
265 initial_store.header.transaction_count + step
266 );
267 assert!(store.checksum_mismatches.is_empty());
268 let index = RevisionIndex::parse(&store).unwrap();
269 for (sid, space) in &index.spaces {
270 let rid = space.labels[&(ExGuid::default(), 1)];
271 if space.revisions[&rid].dependency.is_none() {
272 *checkpoints.entry(*sid).or_insert(0) += 1;
273 }
274 for old_rid in initial_index.spaces[sid].revisions.keys() {
275 assert_eq!(
276 format!("{:?}", initial_index.resolve(*sid, *old_rid).unwrap()),
277 format!("{:?}", index.resolve(*sid, *old_rid).unwrap())
278 );
279 }
280 }
281 if step == 1 || store.header.transaction_count == 256 || step == 512 {
282 let old = current::current(&source);
283 let new = current::current(&written);
284 let mut complete = disk::Disk {
285 visible: source.clone(),
286 durable: source.clone(),
287 operation: 0,
288 fail_at: None,
289 write_limit: 4096,
290 random: 1952,
291 };
292 transaction.commit(&mut complete).unwrap();
293 for fail_at in 1..=complete.operation {
294 let mut interrupted = disk::Disk {
295 visible: source.clone(),
296 durable: source.clone(),
297 operation: 0,
298 fail_at: Some(fail_at),
299 write_limit: 4096,
300 random: 1952 + fail_at as u64,
301 };
302 transaction.commit(&mut interrupted).unwrap_err();
303 let observed = current::current(&interrupted.durable);
304 assert!(observed == old || observed == new, "{step}:{fail_at}");
305 }
306 }
307 source = written;
308 }
309 assert_eq!(checkpoints.len(), 2);
310 assert!(checkpoints.values().all(|count| *count == 1));
311}