1//! A fault-injecting in-memory remote for reconciliation tests.
2#![allow(dead_code)]
3
4use notebook::Remote;
5use onestore::{
6 CommitError, CommitIo, CommitState, ExGuid, RevisionIndex, Stamp, Store, Transaction,
7 document::{Document, Kind},
8};
9use std::io;
10
11#[derive(Clone, Copy, Default)]
12pub enum Fault {
13 #[default]
14 None,
15 Before,
16 UnknownBefore,
17 UnknownAfter,
18 Committed,
19 PanicBefore,
20 PanicAfter,
21 Confirm,
22 ConfirmCommitted,
23}
24
25pub struct Server {
26 pub visible: Vec<u8>,
27 pub durable: Vec<u8>,
28 pub fault: Fault,
29 pub publications: usize,
30 pub confirmations: usize,
31}
32
33impl Server {
34 pub fn new(source: &[u8]) -> Self {
35 Self {
36 visible: source.to_vec(),
37 durable: source.to_vec(),
38 fault: Fault::None,
39 publications: 0,
40 confirmations: 0,
41 }
42 }
43}
44
45pub fn failure(state: CommitState) -> CommitError {
46 CommitError {
47 state,
48 error: io::Error::from(io::ErrorKind::ConnectionAborted),
49 }
50}
51
52impl CommitIo for Server {
53 fn read_at(&mut self, offset: u64, output: &mut [u8]) -> io::Result<usize> {
54 let offset = usize::try_from(offset).unwrap();
55 let size = output.len().min(self.visible.len().saturating_sub(offset));
56 if size > 0 {
57 output[..size].copy_from_slice(&self.visible[offset..offset + size]);
58 }
59 Ok(size)
60 }
61 fn write_at(&mut self, offset: u64, bytes: &[u8]) -> io::Result<usize> {
62 let offset = usize::try_from(offset).unwrap();
63 self.visible
64 .resize(self.visible.len().max(offset + bytes.len()), 0);
65 self.visible[offset..offset + bytes.len()].copy_from_slice(bytes);
66 Ok(bytes.len())
67 }
68 fn flush(&mut self) -> io::Result<()> {
69 self.durable.clone_from(&self.visible);
70 Ok(())
71 }
72}
73
74impl Remote for Server {
75 fn read(&mut self) -> io::Result<Vec<u8>> {
76 Ok(self.visible.clone())
77 }
78 fn stamp(&mut self) -> io::Result<Stamp> {
79 Stamp::of(&self.visible).map_err(io::Error::other)
80 }
81 fn publish(&mut self, transaction: &Transaction) -> Result<(), CommitError> {
82 self.publications += 1;
83 let fault = std::mem::take(&mut self.fault);
84 match fault {
85 Fault::Before => return Err(failure(CommitState::NotCommitted)),
86 Fault::UnknownBefore => return Err(failure(CommitState::Unknown)),
87 Fault::PanicBefore => panic!("Terminated before remote I/O"),
88 _ => {}
89 }
90 let old = self.durable.clone();
91 transaction.commit(self)?;
92 match fault {
93 Fault::UnknownAfter => {
94 self.durable = old;
95 Err(failure(CommitState::Unknown))
96 }
97 Fault::Committed => Err(failure(CommitState::Committed)),
98 Fault::PanicAfter => panic!("Terminated after remote publication"),
99 _ => Ok(()),
100 }
101 }
102 fn confirm(&mut self, base: &Stamp) -> Result<(), CommitError> {
103 self.confirmations += 1;
104 if matches!(self.fault, Fault::Confirm) {
105 self.fault = Fault::None;
106 return Err(failure(CommitState::Unknown));
107 }
108 onestore::confirm(self, base)?;
109 if matches!(self.fault, Fault::ConfirmCommitted) {
110 self.fault = Fault::None;
111 return Err(failure(CommitState::Committed));
112 }
113 Ok(())
114 }
115}
116
117pub fn text(source: &[u8]) -> (ExGuid, ExGuid, String) {
118 let store = Store::parse(source).unwrap();
119 assert!(store.checksum_mismatches.is_empty());
120 let index = RevisionIndex::parse(&store).unwrap();
121 index.validate_current().unwrap();
122 let doc = Document::parse(&index).unwrap();
123 doc.spaces
124 .iter()
125 .find_map(|(sid, space)| {
126 space.revisions[&space.contexts[&ExGuid::default()]]
127 .nodes
128 .iter()
129 .find_map(|(oid, node)| match &node.kind {
130 Kind::RichText { text, .. } => Some((*sid, *oid, text.clone())),
131 _ => None,
132 })
133 })
134 .unwrap()
135}
136
137/// Every page of an image, in section order: what two images holding the same edits under
138/// different revision identities share.
139pub fn pages(source: &[u8]) -> Vec<(ExGuid, onestore::page::Page)> {
140 let store = Store::parse(source).unwrap();
141 let index = RevisionIndex::parse(&store).unwrap();
142 let document = Document::parse(&index).unwrap();
143 document
144 .pages()
145 .unwrap()
146 .into_iter()
147 .map(|(space, _)| {
148 (
149 space,
150 onestore::page::Page::from_space(&document, space).unwrap(),
151 )
152 })
153 .collect()
154}
155
156/// Queues a section op as its own edit.
157pub fn section_op(cache: &notebook::Replica, op: onestore::op::SectionOp) -> u64 {
158 cache
159 .apply(
160 "Fixture",
161 onestore::op::Edit {
162 at: 133_000_000_000_000_000,
163 ops: vec![onestore::op::Op::Section(op)],
164 },
165 )
166 .unwrap()
167}
168
169/// The image the queue leaves, as a recovery archive of the cache records it.
170pub fn snapshot(cache: &notebook::Replica) -> Vec<u8> {
171 archived(cache, |recovery| recovery.snapshot())
172}
173
174/// The remote image the cache last observed, as a recovery archive records it.
175pub fn remote_snapshot(cache: &notebook::Replica) -> Vec<u8> {
176 archived(cache, |recovery| recovery.remote_snapshot())
177}
178
179fn archived(
180 cache: &notebook::Replica,
181 image: impl FnOnce(&notebook::Recovery) -> Result<Vec<u8>, notebook::Error>,
182) -> Vec<u8> {
183 let directory = tempfile::tempdir().unwrap();
184 let path = directory.path().join("recovery.sqlite");
185 cache.export_recovery(&path).unwrap();
186 image(&notebook::Recovery::open(&path).unwrap()).unwrap()
187}
188
189/// `image` after another writer replaces `range` of the text object `text` with `with`.
190pub fn typed(
191 image: &[u8],
192 space: ExGuid,
193 text: ExGuid,
194 range: std::ops::Range<u32>,
195 with: &str,
196) -> Vec<u8> {
197 let op = onestore::op::PageOp::Text {
198 text,
199 range,
200 with: with.into(),
201 };
202 edited(image, vec![onestore::op::Op::Page { space, op }]).unwrap()
203}
204
205/// `image` after another writer's `ops`, sealed as one revision per space they change.
206pub fn edited(image: &[u8], ops: Vec<onestore::op::Op>) -> Result<Vec<u8>, onestore::op::OpError> {
207 let arena = onestore::Arena::default();
208 let mut section =
209 onestore::Section::open(&arena, image.to_vec()).map_err(onestore::op::OpError::Failed)?;
210 let edit = onestore::op::Edit {
211 at: 134_000_000_000_000_000,
212 ops,
213 };
214 section.apply("Other writer", &edit)?;
215 section.seal().map_err(onestore::op::OpError::Failed)?;
216 Ok(section.image())
217}
218
219/// A conflict page as the tests compare it: whose version it is and its texts.
220pub type Version = (String, Vec<String>);
221
222/// Each page's conflict pages in list order.
223pub fn conflicts(image: &[u8]) -> Vec<(ExGuid, Vec<Version>)> {
224 let arena = onestore::Arena::default();
225 let mut section = onestore::Section::open(&arena, image.to_vec()).unwrap();
226 section
227 .conflicts()
228 .unwrap()
229 .into_iter()
230 .map(|(page, conflicts)| {
231 let conflicts = conflicts
232 .into_iter()
233 .map(|conflict| {
234 let page = section.page(conflict.space).unwrap();
235 (conflict.user, page_texts(&page))
236 })
237 .collect();
238 (page, conflicts)
239 })
240 .collect()
241}
242
243/// The texts of a page's outlines and title, in page order.
244pub fn page_texts(page: &onestore::page::Page) -> Vec<String> {
245 use onestore::page::PageObject;
246 page.objects
247 .iter()
248 .flat_map(|object| match object {
249 PageObject::Outline(outline) => outline.paragraphs.clone(),
250 PageObject::Title(title) => title
251 .outlines
252 .iter()
253 .flat_map(|outline| outline.paragraphs.clone())
254 .collect(),
255 _ => Vec::new(),
256 })
257 .filter_map(|paragraph| Some(paragraph.text()?.text.text().to_owned()))
258 .collect()
259}
260
261/// Whether `image` lists a conflict page under page `space`.
262pub fn conflicted(image: &[u8], space: ExGuid) -> bool {
263 conflicts(image).iter().any(|(page, _)| *page == space)
264}