| 1 | #![cfg(feature = "live")] |
| 2 | |
| 3 | use notebook::{ |
| 4 | Replica, |
| 5 | live::share::{HostedRemote, Sharing}, |
| 6 | }; |
| 7 | use onestore::op::{Edit, Op, PageOp}; |
| 8 | use std::sync::{Arc, Barrier}; |
| 9 | |
| 10 | #[path = "support/live.rs"] |
| 11 | mod live; |
| 12 | use live::*; |
| 13 | |
| 14 | #[test] |
| 15 | fn a_lost_batch_receipt_is_confirmed_without_repeating_its_edit() { |
| 16 | use notebook::{PendingEdit, Remote}; |
| 17 | use onestore::{CommitError, CommitState, ExGuid, Stamp, Transaction}; |
| 18 | use std::{collections::BTreeMap, io}; |
| 19 | |
| 20 | struct Lost(HostedRemote, bool); |
| 21 | impl Remote for Lost { |
| 22 | fn read(&mut self) -> io::Result<Vec<u8>> { |
| 23 | self.0.read() |
| 24 | } |
| 25 | fn stamp(&mut self) -> io::Result<Stamp> { |
| 26 | self.0.stamp() |
| 27 | } |
| 28 | fn publish(&mut self, transaction: &Transaction) -> Result<(), CommitError> { |
| 29 | self.0.publish(transaction) |
| 30 | } |
| 31 | fn confirm(&mut self, stamp: &Stamp) -> Result<(), CommitError> { |
| 32 | self.0.confirm(stamp) |
| 33 | } |
| 34 | fn accepts_edits(&self) -> bool { |
| 35 | true |
| 36 | } |
| 37 | fn publish_edits( |
| 38 | &mut self, |
| 39 | transaction: &Transaction, |
| 40 | edits: &[PendingEdit], |
| 41 | revisions: &BTreeMap<ExGuid, ExGuid>, |
| 42 | ) -> Result<(), CommitError> { |
| 43 | self.0.publish_edits(transaction, edits, revisions)?; |
| 44 | if std::mem::take(&mut self.1) { |
| 45 | return Err(CommitError { |
| 46 | state: CommitState::Unknown, |
| 47 | error: io::ErrorKind::BrokenPipe.into(), |
| 48 | }); |
| 49 | } |
| 50 | Ok(()) |
| 51 | } |
| 52 | } |
| 53 | let directory = tempfile::tempdir().unwrap(); |
| 54 | let folder = notebook(directory.path()); |
| 55 | let url = relay(Default::default()); |
| 56 | let sharing = Sharing::new("").unwrap(); |
| 57 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); |
| 58 | let (guest, _) = guest("Alice", &code(&host), &url, &directory.path().join("alice")); |
| 59 | let file = folder.join("Garden.one"); |
| 60 | let image = std::fs::read(&file).unwrap(); |
| 61 | let (space, text, _) = server::text(&image); |
| 62 | let replica = Replica::open_or_create(directory.path().join("replica.sqlite"), None, || { |
| 63 | Ok(image.clone()) |
| 64 | }) |
| 65 | .unwrap(); |
| 66 | let id = replica |
| 67 | .apply( |
| 68 | "Alice", |
| 69 | Edit { |
| 70 | at: 134_000_000_000_000_000, |
| 71 | ops: vec![Op::Page { |
| 72 | space, |
| 73 | op: PageOp::Text { |
| 74 | text, |
| 75 | range: 13..13, |
| 76 | with: "X".into(), |
| 77 | }, |
| 78 | }], |
| 79 | }, |
| 80 | ) |
| 81 | .unwrap(); |
| 82 | let mut remote = Lost(HostedRemote::new(&guest, "Garden.one"), true); |
| 83 | assert!(matches!( |
| 84 | replica.sync_once(&mut remote), |
| 85 | Err(notebook::Error::Remote(CommitError { |
| 86 | state: CommitState::Unknown, |
| 87 | .. |
| 88 | })) |
| 89 | )); |
| 90 | replica.sync_once(&mut remote).unwrap(); |
| 91 | assert!(matches!( |
| 92 | replica.status(id).unwrap(), |
| 93 | Some(notebook::EditStatus::Published { .. }) |
| 94 | )); |
| 95 | assert_eq!( |
| 96 | server::text(&std::fs::read(file).unwrap()).2, |
| 97 | "Original textX" |
| 98 | ); |
| 99 | assert!(replica.pending().unwrap().is_empty()); |
| 100 | } |
| 101 | |
| 102 | #[test] |
| 103 | fn concurrent_batches_keep_receipts_and_rebase_later_keystrokes() { |
| 104 | let directory = tempfile::tempdir().unwrap(); |
| 105 | let folder = notebook(directory.path()); |
| 106 | let url = relay(Default::default()); |
| 107 | let sharing = Sharing::new("").unwrap(); |
| 108 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); |
| 109 | let code = code(&host); |
| 110 | let (alice, _) = guest("Alice", &code, &url, &directory.path().join("alice")); |
| 111 | let (bob, _) = guest("Bob", &code, &url, &directory.path().join("bob")); |
| 112 | let file = folder.join("Garden.one"); |
| 113 | let original = std::fs::read(&file).unwrap(); |
| 114 | let (space, text, _) = server::text(&original); |
| 115 | let edit = |with: &str, at| Edit { |
| 116 | at: 134_000_000_000_000_000, |
| 117 | ops: vec![Op::Page { |
| 118 | space, |
| 119 | op: PageOp::Text { |
| 120 | text, |
| 121 | range: at..at, |
| 122 | with: with.into(), |
| 123 | }, |
| 124 | }], |
| 125 | }; |
| 126 | let a = Arc::new( |
| 127 | Replica::open_or_create(directory.path().join("a.sqlite"), None, || { |
| 128 | Ok(original.clone()) |
| 129 | }) |
| 130 | .unwrap(), |
| 131 | ); |
| 132 | let b = Arc::new( |
| 133 | Replica::open_or_create(directory.path().join("b.sqlite"), None, || { |
| 134 | Ok(original.clone()) |
| 135 | }) |
| 136 | .unwrap(), |
| 137 | ); |
| 138 | let first = a.apply("Alice", edit("A", 13)).unwrap(); |
| 139 | let second = b.apply("Bob", edit("B", 0)).unwrap(); |
| 140 | let barrier = Arc::new(Barrier::new(2)); |
| 141 | std::thread::scope(|scope| { |
| 142 | for (replica, guest) in [(&a, &alice), (&b, &bob)] { |
| 143 | let barrier = Arc::clone(&barrier); |
| 144 | scope.spawn(move || { |
| 145 | barrier.wait(); |
| 146 | replica |
| 147 | .sync_once(&mut HostedRemote::new(guest, "Garden.one")) |
| 148 | .unwrap(); |
| 149 | }); |
| 150 | } |
| 151 | }); |
| 152 | assert!(matches!( |
| 153 | a.status(first).unwrap(), |
| 154 | Some(notebook::EditStatus::Published { .. }) |
| 155 | )); |
| 156 | assert!(matches!( |
| 157 | b.status(second).unwrap(), |
| 158 | Some(notebook::EditStatus::Published { .. }) |
| 159 | )); |
| 160 | let next = server::text(&a.snapshot().unwrap()).2.find('A').unwrap() as u32 + 1; |
| 161 | let third = a.apply("Alice", edit("a", next)).unwrap(); |
| 162 | let mut remote = HostedRemote::new(&alice, "Garden.one"); |
| 163 | for _ in 0..5 { |
| 164 | a.sync_once(&mut remote).unwrap(); |
| 165 | if matches!( |
| 166 | a.status(third).unwrap(), |
| 167 | Some(notebook::EditStatus::Published { .. }) |
| 168 | ) { |
| 169 | break; |
| 170 | } |
| 171 | } |
| 172 | let result = std::fs::read(&file).unwrap(); |
| 173 | let result_text = server::text(&result).2; |
| 174 | assert!(result_text.contains("Aa"), "{result_text}"); |
| 175 | assert!(result_text.contains('B'), "{result_text}"); |
| 176 | let arena = onestore::Arena::default(); |
| 177 | let mut section = onestore::Section::open(&arena, result).unwrap(); |
| 178 | assert!(section.conflicts().unwrap().is_empty()); |
| 179 | } |