| 1 | //! A fault-injecting in-memory remote for reconciliation tests. |
| 2 | #![allow(dead_code)] |
| 3 | |
| 4 | use notebook::Remote; |
| 5 | use onestore::{ |
| 6 | CommitError, CommitIo, CommitState, ExGuid, RevisionIndex, Stamp, Store, Transaction, |
| 7 | document::{Document, Kind}, |
| 8 | }; |
| 9 | use std::io; |
| 10 | |
| 11 | #[derive(Clone, Copy, Default)] |
| 12 | pub enum Fault { |
| 13 | #[default] |
| 14 | None, |
| 15 | Before, |
| 16 | UnknownBefore, |
| 17 | UnknownAfter, |
| 18 | Committed, |
| 19 | PanicBefore, |
| 20 | PanicAfter, |
| 21 | Confirm, |
| 22 | ConfirmCommitted, |
| 23 | } |
| 24 | |
| 25 | pub 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 | |
| 33 | impl 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 | |
| 45 | pub fn failure(state: CommitState) -> CommitError { |
| 46 | CommitError { |
| 47 | state, |
| 48 | error: io::Error::from(io::ErrorKind::ConnectionAborted), |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | impl 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 | |
| 74 | impl 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 | |
| 117 | pub 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. |
| 139 | pub 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. |
| 157 | pub 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. |
| 170 | pub 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. |
| 175 | pub fn remote_snapshot(cache: &notebook::Replica) -> Vec<u8> { |
| 176 | archived(cache, |recovery| recovery.remote_snapshot()) |
| 177 | } |
| 178 | |
| 179 | fn 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`. |
| 190 | pub 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. |
| 206 | pub 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. |
| 220 | pub type Version = (String, Vec<String>); |
| 221 | |
| 222 | /// Each page's conflict pages in list order. |
| 223 | pub 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. |
| 244 | pub 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`. |
| 262 | pub fn conflicted(image: &[u8], space: ExGuid) -> bool { |
| 263 | conflicts(image).iter().any(|(page, _)| *page == space) |
| 264 | } |