1#[path = "support/checkpoint.rs"]
2mod checkpoint;
3#[path = "support/current.rs"]
4mod current;
5#[path = "support/disk.rs"]
6mod disk;
7#[path = "support/ops.rs"]
8mod ops;
9#[path = "support/sweep.rs"]
10mod sweep;
11
12use onestore::{
13 ExGuid, RevisionIndex, Store, TextAttribute as A,
14 document::{Document, Kind},
15 op::{Edit, Op, PageOp},
16};
17use std::ops::Range;
18
19/// Where an insertion goes: a paragraph in a container before a child of it, or last; or a
20/// new outline at a point on the page.
21#[derive(Clone, Copy)]
22enum Place {
23 In(ExGuid, Option<ExGuid>),
24 Outline(f32, f32),
25}
26
27/// The ops inserting `text` at `place` with `formats` set over UTF-16 ranges of it, and the
28/// identities of the new paragraph (or outline) and text.
29fn insertion(
30 space: ExGuid,
31 place: Place,
32 text: &str,
33 formats: &[(Range<u32>, Vec<A>)],
34) -> (Vec<Op>, ExGuid, ExGuid) {
35 let paragraph = ops::paragraph(text);
36 let text = paragraph.text().unwrap().id;
37 let (op, object) = match place {
38 Place::In(container, before) => {
39 let id = paragraph.id;
40 let paragraphs = vec![paragraph];
41 (
42 PageOp::Insert {
43 container,
44 before,
45 paragraphs,
46 },
47 id,
48 )
49 }
50 Place::Outline(x, y) => {
51 let object = ops::outline(x, y, vec![paragraph]);
52 let id = object.id();
53 (
54 PageOp::Add {
55 object,
56 before: None,
57 },
58 id,
59 )
60 }
61 };
62 let mut result = vec![Op::Page { space, op }];
63 for (range, set) in formats {
64 let op = PageOp::Format {
65 text,
66 range: range.clone(),
67 set: set.clone(),
68 clear: Vec::new(),
69 };
70 result.push(Op::Page { space, op });
71 }
72 (result, object, text)
73}
74
75fn targets(source: &[u8]) -> (ExGuid, ExGuid, ExGuid, ExGuid, ExGuid) {
76 let store = Store::parse(source).unwrap();
77 let index = RevisionIndex::parse(&store).unwrap();
78 let document = Document::parse(&index).unwrap();
79 let (sid, page) = document.pages().unwrap()[0];
80 let space = &document.spaces[&sid];
81 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
82 let outline = view.nodes[&page]
83 .children
84 .iter()
85 .copied()
86 .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. }))
87 .unwrap();
88 let paragraph = view.nodes[&outline].children[0];
89 let text = view.nodes[&paragraph].content[0];
90 (sid, page, outline, paragraph, text)
91}
92
93#[test]
94fn atomic_formatted_insertions_match_a_character_model_and_preserve_history() {
95 use serde_json::json;
96 let source = onestore::create_section("rich.one", "Original", "Author").unwrap();
97 let (sid, page, outline, _, _) = targets(&source);
98 let text = "abπŸ¦€ e\u{301} 東京\rEnd\t!";
99 let (plain, _, plain_text) = insertion(sid, Place::In(outline, None), text, &[]);
100 let base = ops::edited(&source, plain).unwrap();
101 let base_store = Store::parse(&base).unwrap();
102 let base_index = RevisionIndex::parse(&base_store).unwrap();
103 let base_doc = Document::parse(&base_index).unwrap();
104 let base_space = &base_doc.spaces[&sid];
105 let base_view = &base_space.revisions[&base_space.contexts[&ExGuid::default()]];
106 let default =
107 serde_json::to_value(&base_view.text_runs(plain_text).unwrap()[0].format).unwrap();
108 let offsets: Vec<u32> = std::iter::once(0)
109 .chain(text.chars().scan(0, |n, c| {
110 *n += c.len_utf16() as u32;
111 Some(*n)
112 }))
113 .collect();
114 let old_store = Store::parse(&source).unwrap();
115 let old_index = RevisionIndex::parse(&old_store).unwrap();
116 for seed in sweep::seeds(1..49, 48) {
117 let mut random = seed;
118 let mut expected: Vec<_> = text.chars().map(|c| (c, default.clone())).collect();
119 let mut formats = Vec::new();
120 // Descending construction also exercises the order formats apply in.
121 for i in (0..expected.len()).rev() {
122 random ^= random << 13;
123 random ^= random >> 7;
124 random ^= random << 17;
125 let (attributes, values) = match random % 8 {
126 0 => continue,
127 1 => (
128 vec![A::Bold(true), A::Underline(true)],
129 json!({"bold":true,"underline":true}),
130 ),
131 2 => (
132 vec![A::Italic(true), A::Strike(true)],
133 json!({"italic":true,"strike":true}),
134 ),
135 3 => (
136 vec![A::Font("Arial".into()), A::FontSize(13.5)],
137 json!({"font":"Arial","font_size":13.5}),
138 ),
139 4 => (
140 vec![
141 A::Color(Some([12, 34, 56])),
142 A::Highlight(Some([255, 255, 0])),
143 ],
144 json!({"color":0x38220c,"highlight":0x00ffff}),
145 ),
146 5 => (
147 vec![A::Superscript(true)],
148 json!({"superscript":true,"subscript":false}),
149 ),
150 6 => (
151 vec![A::Subscript(true)],
152 json!({"subscript":true,"superscript":false}),
153 ),
154 _ => (
155 vec![A::Bold(false), A::Color(None), A::Highlight(None)],
156 json!({"bold":false,"color":0xff000000_u32,"highlight":0xff000000_u32}),
157 ),
158 };
159 formats.push((offsets[i]..offsets[i + 1], attributes));
160 for (key, value) in values.as_object().unwrap() {
161 expected[i].1[key] = value.clone();
162 }
163 }
164 let place = if seed % 2 == 0 {
165 Place::Outline(72.0, 144.0)
166 } else {
167 Place::In(outline, None)
168 };
169 let (ops, _, inserted) = insertion(sid, place, text, &formats);
170 let edit = Edit {
171 at: ops::now(),
172 ops,
173 };
174 let restored: Edit = serde_json::from_slice(&serde_json::to_vec(&edit).unwrap()).unwrap();
175 assert_eq!(restored, edit);
176 let edited = ops::edited(&source, restored.ops).unwrap();
177 let store = Store::parse(&edited).unwrap();
178 assert_eq!(
179 store.header.transaction_count,
180 old_store.header.transaction_count + 1
181 );
182 let index = RevisionIndex::parse(&store).unwrap();
183 index.validate_current().unwrap();
184 assert_eq!(
185 index.spaces[&sid].revisions.len(),
186 old_index.spaces[&sid].revisions.len() + 1
187 );
188 for (space, old) in &old_index.spaces {
189 for revision in old.revisions.keys() {
190 assert_eq!(
191 format!("{:?}", old_index.resolve(*space, *revision).unwrap()),
192 format!("{:?}", index.resolve(*space, *revision).unwrap())
193 );
194 }
195 }
196 let document = Document::parse(&index).unwrap();
197 let space = &document.spaces[&sid];
198 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
199 if let Place::Outline(..) = place {
200 assert!(view.nodes[&page].children.len() > 1);
201 }
202 let actual: Vec<_> = view
203 .text_runs(inserted)
204 .unwrap()
205 .into_iter()
206 .flat_map(|run| {
207 let format = serde_json::to_value(run.format).unwrap();
208 run.text.chars().map(move |c| (c, format.clone()))
209 })
210 .collect();
211 assert_eq!(actual, expected, "seed {seed}");
212 }
213}
214
215#[test]
216fn formatted_empty_insertions_preserve_the_insertion_style_for_later_typing() {
217 let source = onestore::create_section("empty-rich.one", "Original", "Author").unwrap();
218 let (sid, _, outline, _, _) = targets(&source);
219 for place in [Place::Outline(72.0, 144.0), Place::In(outline, None)] {
220 let style = [(0..0, vec![A::Italic(true), A::FontSize(18.0)])];
221 let (ops, _, text) = insertion(sid, place, "", &style);
222 let inserted = ops::edited(&source, ops).unwrap();
223 let typed = ops::page_edited(
224 &inserted,
225 sid,
226 vec![PageOp::Text {
227 text,
228 range: 0..0,
229 with: "Typed πŸ¦€".into(),
230 }],
231 )
232 .unwrap();
233 for (bytes, expected) in [(&inserted, ""), (&typed, "Typed πŸ¦€")] {
234 let store = Store::parse(bytes).unwrap();
235 let index = RevisionIndex::parse(&store).unwrap();
236 index.validate_current().unwrap();
237 let document = Document::parse(&index).unwrap();
238 let space = &document.spaces[&sid];
239 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
240 let runs = view.text_runs(text).unwrap();
241 assert_eq!(runs.len(), 1);
242 assert_eq!(runs[0].text, expected);
243 assert_eq!(runs[0].format.italic, Some(true));
244 assert_eq!(runs[0].format.font_size, Some(18.0));
245 }
246 }
247}
248
249#[test]
250#[ignore = "exports formatted insertion candidates for cold native validation"]
251fn export_native_formatted_insertions() {
252 use std::{fs, path::PathBuf};
253 let output = PathBuf::from(std::env::var_os("ONESTORE_INSERT_OUTPUT").unwrap());
254 assert!(output.is_absolute());
255 fs::create_dir(&output).unwrap();
256 let generated = onestore::create_section("rich.one", "Original", "Author").unwrap();
257 let unicode = include_bytes!(
258 "../../../corpus/native/20260905-05/snapshots/03-format-unicode/notebook/synthetic.one"
259 );
260 let table = include_bytes!(
261 "../../../corpus/native/20260905-05/snapshots/07-table/notebook/synthetic.one"
262 );
263 let text = "Bold πŸ¦€ italic e\u{301} color 東京\rEnd";
264 let mut manifest = Vec::new();
265 for (name, source, empty) in [
266 ("outline", generated.as_slice(), false),
267 ("native-paragraph", unicode.as_slice(), false),
268 ("native-cell", table.as_slice(), false),
269 ("empty", generated.as_slice(), true),
270 ("empty-typed", generated.as_slice(), true),
271 ] {
272 let (sid, _, outline, paragraph, _) = targets(source);
273 let place = match name {
274 "native-paragraph" => Place::In(outline, Some(paragraph)),
275 "native-cell" => {
276 let store = Store::parse(source).unwrap();
277 let index = RevisionIndex::parse(&store).unwrap();
278 let document = Document::parse(&index).unwrap();
279 let space = &document.spaces[&sid];
280 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
281 let cell = *view
282 .nodes
283 .iter()
284 .find(|(_, node)| matches!(node.kind, Kind::Cell { .. }))
285 .unwrap()
286 .0;
287 Place::In(cell, None)
288 }
289 _ => Place::Outline(72.0, 144.0),
290 };
291 let formats = if empty {
292 vec![(0..0, vec![A::Italic(true), A::FontSize(18.0)])]
293 } else {
294 [
295 ("Bold", vec![A::Bold(true), A::Underline(true)]),
296 ("πŸ¦€", vec![A::Font("Arial".into()), A::FontSize(13.5)]),
297 ("italic", vec![A::Italic(true), A::Strike(true)]),
298 ("e\u{301}", vec![A::Superscript(true)]),
299 (
300 "color",
301 vec![
302 A::Color(Some([12, 34, 56])),
303 A::Highlight(Some([255, 255, 0])),
304 ],
305 ),
306 ("東京", vec![A::Subscript(true)]),
307 (
308 "End",
309 vec![
310 A::Bold(false),
311 A::Italic(false),
312 A::Color(None),
313 A::Highlight(None),
314 ],
315 ),
316 ]
317 .into_iter()
318 .map(|(label, attributes)| {
319 let start = text[..text.find(label).unwrap()].encode_utf16().count() as u32;
320 (
321 start..start + label.encode_utf16().count() as u32,
322 attributes,
323 )
324 })
325 .collect()
326 };
327 let (mut ops, object, inserted) =
328 insertion(sid, place, if empty { "" } else { text }, &formats);
329 if name == "empty-typed" {
330 let op = PageOp::Text {
331 text: inserted,
332 range: 0..0,
333 with: "Typed café πŸ¦€".into(),
334 };
335 ops.push(Op::Page { space: sid, op });
336 }
337 let edit = Edit {
338 at: ops::now(),
339 ops,
340 };
341 let bytes = ops::edited(source, edit.ops.clone()).unwrap();
342 current::current(&bytes);
343 let notebook = output.join(name);
344 fs::create_dir(&notebook).unwrap();
345 fs::write(notebook.join("synthetic.one"), &bytes).unwrap();
346 manifest.push(serde_json::json!({
347 "name": name, "space": sid, "object": object, "text": inserted, "edit": edit
348 }));
349 }
350 fs::write(
351 output.join("manifest.json"),
352 serde_json::to_vec_pretty(&manifest).unwrap(),
353 )
354 .unwrap();
355}
356
357#[test]
358fn paragraph_and_outline_insertions_publish_metadata_and_references_together() {
359 let source = onestore::create_section("insertion.one", "Original", "Original author").unwrap();
360 let (sid, page, outline, paragraph, original_text) = targets(&source);
361 for (place, expected_parent, expected_x, expected_y) in [
362 (Place::In(outline, Some(paragraph)), outline, None, None),
363 (Place::Outline(144.0, 18.0), page, Some(144.0), Some(18.0)),
364 ] {
365 let (ops, object, text) = insertion(sid, place, "First πŸ¦€\rSecond", &[]);
366 let transaction = ops::transaction(&source, "New author", ops)
367 .unwrap()
368 .unwrap();
369 let mut written = source.clone();
370 transaction.apply(&mut written).unwrap();
371 let store = Store::parse(&written).unwrap();
372 assert_eq!(
373 store.header.transaction_count,
374 Store::parse(&source).unwrap().header.transaction_count + 1
375 );
376 let index = RevisionIndex::parse(&store).unwrap();
377 index.validate_current().unwrap();
378 let document = Document::parse(&index).unwrap();
379 let space = &document.spaces[&sid];
380 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
381 assert!(view.nodes[&expected_parent].children.contains(&object));
382 assert_eq!(view.nodes[&object].layout.x, expected_x);
383 assert_eq!(view.nodes[&object].layout.y, expected_y);
384 assert!(
385 matches!(&view.nodes[&original_text].kind, Kind::RichText { text, .. } if text == "Original")
386 );
387 assert!(
388 matches!(&view.nodes[&text].kind, Kind::RichText { text, .. } if text == "First πŸ¦€\rSecond")
389 );
390 assert!(
391 matches!(&view.nodes[&page].kind, Kind::Page { alternate_title, .. } if alternate_title.as_deref() == Some("First πŸ¦€"))
392 );
393 assert!(
394 matches!(&view.nodes[&view.roots[&2]].kind, Kind::Metadata { title, .. } if title.as_deref() == Some("First πŸ¦€"))
395 );
396 assert!(
397 view.nodes
398 .values()
399 .any(|node| matches!(&node.kind, Kind::Author { name } if name.as_deref() == Some("New author")))
400 );
401 let runs = view.text_runs(text).unwrap();
402 assert_eq!(runs.len(), 1);
403 assert_eq!(runs[0].format.font.as_deref(), Some("Calibri"));
404 assert_eq!(runs[0].format.font_size, Some(11.0));
405 let previous_store = Store::parse(&source).unwrap();
406 let previous = RevisionIndex::parse(&previous_store).unwrap();
407 for (old_sid, old_space) in &previous.spaces {
408 for rid in old_space.revisions.keys() {
409 assert_eq!(
410 format!("{:?}", previous.resolve(*old_sid, *rid).unwrap()),
411 format!("{:?}", index.resolve(*old_sid, *rid).unwrap())
412 );
413 }
414 }
415 }
416}
417
418#[test]
419fn repeated_insertions_and_formatting_share_immutable_objects() {
420 let mut source = onestore::create_section("shared.one", "Original", "Same author").unwrap();
421 let (sid, _, outline, _, _) = targets(&source);
422 let mut texts = Vec::new();
423 for _ in 0..12 {
424 let (ops, _, text) = insertion(sid, Place::In(outline, None), "Repeated paragraph", &[]);
425 let transaction = ops::transaction(&source, "Same author", ops)
426 .unwrap()
427 .unwrap();
428 transaction.apply(&mut source).unwrap();
429 let bold = PageOp::Format {
430 text,
431 range: 1..8,
432 set: vec![A::Bold(true), A::FontSize(20.0)],
433 clear: Vec::new(),
434 };
435 let transaction = ops::transaction(
436 &source,
437 "Same author",
438 vec![Op::Page {
439 space: sid,
440 op: bold,
441 }],
442 )
443 .unwrap()
444 .unwrap();
445 transaction.apply(&mut source).unwrap();
446 texts.push(text);
447 }
448 let store = Store::parse(&source).unwrap();
449 let index = RevisionIndex::parse(&store).unwrap();
450 index.validate_current().unwrap();
451 let raw = index
452 .resolve(sid, index.spaces[&sid].labels[&(ExGuid::default(), 1)])
453 .unwrap();
454 let mut unique = std::collections::BTreeSet::new();
455 let mut counts = Vec::new();
456 for id in raw.reachable().unwrap() {
457 let object = &raw.objects[&id];
458 if object.jcid & 0x100000 == 0 {
459 continue;
460 }
461 let onestore::ObjectData::Properties(bytes) = object.data else {
462 panic!()
463 };
464 assert!(
465 unique.insert((object.jcid, bytes.to_vec())),
466 "Duplicate immutable object"
467 );
468 counts.push((object.jcid, object.reference_count));
469 }
470 counts.sort();
471 assert_eq!(counts, [(0x120001, 26), (0x12004d, 12), (0x12004d, 25)]);
472 let document = Document::parse(&index).unwrap();
473 let space = &document.spaces[&sid];
474 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
475 for text in texts {
476 let runs = view.text_runs(text).unwrap();
477 assert_eq!(runs.len(), 3);
478 assert_eq!(runs[1].format.bold, Some(true));
479 assert_eq!(runs[1].format.font_size, Some(20.0));
480 }
481}
482
483#[test]
484fn serialized_insertions_rebase_with_the_same_objects_and_preserve_remote_edits() {
485 let source = onestore::create_section("rebase.one", "Original", "Author").unwrap();
486 let (sid, _, outline, paragraph, text) = targets(&source);
487 let formats = [(0..3, vec![A::Bold(true), A::Color(Some([12, 34, 56]))])];
488 let (ops, object, inserted) = insertion(
489 sid,
490 Place::In(outline, Some(paragraph)),
491 "Inserted",
492 &formats,
493 );
494 let encoded = serde_json::to_vec(&ops).unwrap();
495 let restored: Vec<Op> = serde_json::from_slice(&encoded).unwrap();
496 assert_eq!(ops, restored);
497 let original = ops::edited(&source, ops).unwrap();
498 let remote = ops::page_edited(
499 &source,
500 sid,
501 vec![PageOp::Text {
502 text,
503 range: 0..0,
504 with: "Remote ".into(),
505 }],
506 )
507 .unwrap();
508 let updated = ops::edited(&remote, restored.clone()).unwrap();
509 let store = Store::parse(&updated).unwrap();
510 let index = RevisionIndex::parse(&store).unwrap();
511 let document = Document::parse(&index).unwrap();
512 let space = &document.spaces[&sid];
513 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
514 assert_eq!(view.nodes[&outline].children, [object, paragraph]);
515 assert!(
516 matches!(&view.nodes[&text].kind, Kind::RichText { text, .. } if text == "Remote Original")
517 );
518 assert!(
519 matches!(&view.nodes[&inserted].kind, Kind::RichText { text, .. } if text == "Inserted")
520 );
521 let runs = view.text_runs(inserted).unwrap();
522 assert_eq!(runs[0].text, "Ins");
523 assert_eq!(runs[0].format.bold, Some(true));
524 assert_eq!(runs[0].format.color, Some(0x38220c));
525 assert_eq!(runs[1].text, "erted");
526 assert_ne!(runs[1].format.bold, Some(true));
527 assert!(ops::edited(&original, restored.clone()).is_err());
528 assert!(ops::edited(&updated, restored).is_err());
529}
530
531#[test]
532#[cfg(any(unix, windows))]
533fn filesystem_insertion_commits_once_and_rejects_stale_replay() {
534 use std::{fs, io::Write};
535 let source = onestore::create_section("file.one", "Original", "Author").unwrap();
536 let (sid, _, outline, _, _) = targets(&source);
537 let (ops, object, _) = insertion(sid, Place::In(outline, None), "File insertion", &[]);
538 let path = std::env::temp_dir().join(format!("onestore-insertion-{object}.one"));
539 let mut file = fs::OpenOptions::new()
540 .write(true)
541 .create_new(true)
542 .open(&path)
543 .unwrap();
544 file.write_all(&source).unwrap();
545 file.sync_all().unwrap();
546 drop(file);
547 let transaction = ops::transaction(&source, "Author", ops).unwrap().unwrap();
548 let result = transaction.commit_file(&path);
549 let written = onestore::read_file(&path).unwrap();
550 let repeated = transaction.commit_file(&path).unwrap_err();
551 fs::remove_file(path).unwrap();
552 result.unwrap();
553 let mut expected = source.clone();
554 transaction.apply(&mut expected).unwrap();
555 assert_eq!(written, expected);
556 assert_eq!(repeated.state, onestore::CommitState::NotCommitted);
557 assert_eq!(repeated.error.kind(), std::io::ErrorKind::ResourceBusy);
558}
559
560#[test]
561fn anchors_targets_identities_and_text_are_validated_before_publication() {
562 let source = onestore::create_section("invalid.one", "Original", "Author").unwrap();
563 let (sid, page, outline, _, text) = targets(&source);
564 let refused = |source: &[u8], place, content: &str| {
565 ops::edited(source, insertion(sid, place, content, &[]).0).is_err()
566 };
567 assert!(refused(&source, Place::Outline(f32::NAN, 0.0), "Text"));
568 assert!(refused(&source, Place::Outline(0.0, f32::INFINITY), "Text"));
569 for content in ["a\0b", "a\nb", "a\u{fffc}b"] {
570 assert!(refused(&source, Place::In(outline, None), content));
571 }
572 assert!(
573 ops::transaction(
574 &source,
575 "a\0b",
576 insertion(sid, Place::In(outline, None), "Text", &[]).0
577 )
578 .is_err()
579 );
580 for place in [
581 Place::In(outline, Some(text)),
582 Place::In(text, None),
583 Place::In(page, None),
584 ] {
585 assert!(refused(&source, place, "Text"));
586 }
587 let (ops, object, _) = insertion(sid, Place::In(outline, None), "Text", &[]);
588 let created = ops::edited(&source, ops.clone()).unwrap();
589 let before_created = Place::In(outline, Some(object));
590 assert!(refused(&source, before_created, "Anchored"));
591 assert!(!refused(&created, before_created, "Anchored"));
592 // A new object's identity may not already be on the page.
593 assert!(ops::edited(&created, ops).is_err());
594}
595
596#[test]
597fn insertions_into_nested_paragraphs_and_native_table_cells_preserve_structure() {
598 let source = onestore::create_section("nested.one", "Original", "Author").unwrap();
599 let (sid, _, outline, paragraph, _) = targets(&source);
600 let mut nested = ops::paragraph("Nested");
601 nested.level = 2;
602 let child = nested.id;
603 let insert = PageOp::Insert {
604 container: paragraph,
605 before: None,
606 paragraphs: vec![nested],
607 };
608 let changed = ops::page_edited(&source, sid, vec![insert]).unwrap();
609 let store = Store::parse(&changed).unwrap();
610 let index = RevisionIndex::parse(&store).unwrap();
611 let document = Document::parse(&index).unwrap();
612 let space = &document.spaces[&sid];
613 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
614 assert_eq!(view.nodes[&outline].children, [paragraph]);
615 assert_eq!(view.nodes[&paragraph].children, [child]);
616 let source = include_bytes!(
617 "../../../corpus/native/20260905-05/snapshots/07-table/notebook/synthetic.one"
618 );
619 let store = Store::parse(source).unwrap();
620 let index = RevisionIndex::parse(&store).unwrap();
621 let document = Document::parse(&index).unwrap();
622 let (sid, cell) = document
623 .spaces
624 .iter()
625 .find_map(|(sid, space)| {
626 let view = &space.revisions[&space.contexts[&ExGuid::default()]];
627 view.nodes.iter().find_map(|(id, node)| {
628 matches!(node.kind, Kind::Cell { .. }).then_some((*sid, *id))
629 })
630 })
631 .unwrap();
632 let (ops, _, _) = insertion(sid, Place::In(cell, None), "Added to cell", &[]);
633 current::current(&ops::edited(source, ops).unwrap());
634}
635
636#[test]
637fn insertion_publication_faults_expose_only_complete_graphs_and_title_caches() {
638 let original = onestore::create_section("atomic.one", "Original", "Author").unwrap();
639 let (sid, _, _, _, text) = targets(&original);
640 let checkpoint = checkpoint::pending(&original, sid, text);
641 for source in [
642 original.as_slice(),
643 include_bytes!("../../../corpus/append/round-01/tx-255/notebook/synthetic.one"),
644 &checkpoint,
645 ] {
646 let (sid, _, outline, paragraph, _) = targets(source);
647 for place in [
648 Place::In(outline, Some(paragraph)),
649 Place::Outline(0.0, 0.0),
650 ] {
651 let formats = [
652 (0..3, vec![A::Bold(true), A::FontSize(18.0)]),
653 (
654 6..8,
655 vec![A::Italic(true), A::Highlight(Some([255, 255, 0]))],
656 ),
657 ];
658 let (ops, _, _) = insertion(sid, place, "First πŸ¦€", &formats);
659 let edit = ops::transaction(source, "New author", ops)
660 .unwrap()
661 .unwrap();
662 let mut written = source.to_vec();
663 edit.apply(&mut written).unwrap();
664 let before = current::current(source);
665 let after = current::current(&written);
666 for write_limit in [17, 4096] {
667 let disk = |fail_at| disk::Disk {
668 visible: source.to_vec(),
669 durable: source.to_vec(),
670 operation: 0,
671 fail_at,
672 write_limit,
673 random: 945,
674 };
675 let mut successful = disk(None);
676 edit.commit(&mut successful).unwrap();
677 assert_eq!(successful.durable, written);
678 for at in 1..=successful.operation {
679 let mut interrupted = disk(Some(at));
680 let failure = edit.commit(&mut interrupted).unwrap_err();
681 let state = current::current(&interrupted.durable);
682 assert!(state == before || state == after, "interruption {at}");
683 if failure.state == onestore::CommitState::NotCommitted {
684 assert_eq!(state, before);
685 }
686 if failure.state == onestore::CommitState::Committed {
687 assert_eq!(state, after);
688 }
689 }
690 }
691 }
692 }
693}