| 1 | #[path = "support/ops.rs"] |
| 2 | mod ops; |
| 3 | |
| 4 | use onestore::{ |
| 5 | CommitIo, CommitState, ExGuid, RevisionIndex, Stamp, Store, Transaction, |
| 6 | document::{Document, Kind}, |
| 7 | op::{Op, PageOp}, |
| 8 | }; |
| 9 | use std::io; |
| 10 | |
| 11 | /// Crosses the 65535 → 65536 counter carry on its next transaction. |
| 12 | const SOURCE: &[u8] = |
| 13 | include_bytes!("../../../corpus/append/round-01/tx-65535/notebook/synthetic.one"); |
| 14 | |
| 15 | fn text(source: &[u8]) -> (ExGuid, ExGuid) { |
| 16 | let store = Store::parse(source).unwrap(); |
| 17 | let index = RevisionIndex::parse(&store).unwrap(); |
| 18 | let document = Document::parse(&index).unwrap(); |
| 19 | for (sid, space) in &document.spaces { |
| 20 | let revision = &space.revisions[&space.contexts[&ExGuid::default()]]; |
| 21 | for (oid, node) in &revision.nodes { |
| 22 | if matches!(node.kind, Kind::RichText { .. }) { |
| 23 | return (*sid, *oid); |
| 24 | } |
| 25 | } |
| 26 | } |
| 27 | panic!("Missing text fixture") |
| 28 | } |
| 29 | |
| 30 | /// The transaction typing `with` at the start of the text object, and the image it leaves. |
| 31 | fn typed(space: ExGuid, text: ExGuid, with: &str) -> (Transaction, Vec<u8>) { |
| 32 | let op = PageOp::Text { |
| 33 | text, |
| 34 | range: 0..0, |
| 35 | with: with.into(), |
| 36 | }; |
| 37 | let transaction = ops::transaction(SOURCE, "Author", vec![Op::Page { space, op }]) |
| 38 | .unwrap() |
| 39 | .unwrap(); |
| 40 | let mut image = SOURCE.to_vec(); |
| 41 | transaction.apply(&mut image).unwrap(); |
| 42 | (transaction, image) |
| 43 | } |
| 44 | |
| 45 | /// A file that records how many bytes a commit reads and writes. |
| 46 | struct Counted { |
| 47 | bytes: Vec<u8>, |
| 48 | read: usize, |
| 49 | written: usize, |
| 50 | } |
| 51 | |
| 52 | impl CommitIo for Counted { |
| 53 | fn read_at(&mut self, offset: u64, output: &mut [u8]) -> io::Result<usize> { |
| 54 | let offset = offset as usize; |
| 55 | let count = output.len().min(self.bytes.len().saturating_sub(offset)); |
| 56 | output[..count].copy_from_slice(&self.bytes[offset..offset + count]); |
| 57 | self.read += count; |
| 58 | Ok(count) |
| 59 | } |
| 60 | fn write_at(&mut self, offset: u64, bytes: &[u8]) -> io::Result<usize> { |
| 61 | let offset = offset as usize; |
| 62 | self.bytes |
| 63 | .resize(self.bytes.len().max(offset + bytes.len()), 0); |
| 64 | self.bytes[offset..offset + bytes.len()].copy_from_slice(bytes); |
| 65 | self.written += bytes.len(); |
| 66 | Ok(bytes.len()) |
| 67 | } |
| 68 | fn flush(&mut self) -> io::Result<()> { |
| 69 | Ok(()) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | #[test] |
| 74 | fn a_transaction_reads_the_header_and_writes_only_its_edit() { |
| 75 | let (sid, oid) = text(SOURCE); |
| 76 | let (transaction, written) = typed(sid, oid, "é"); |
| 77 | let mut image = SOURCE.to_vec(); |
| 78 | transaction.apply(&mut image).unwrap(); |
| 79 | assert_eq!(image, written); |
| 80 | let mut file = Counted { |
| 81 | bytes: SOURCE.to_vec(), |
| 82 | read: 0, |
| 83 | written: 0, |
| 84 | }; |
| 85 | transaction.commit(&mut file).unwrap(); |
| 86 | assert_eq!(file.bytes, written); |
| 87 | assert!(file.read <= 1024 + 2, "read {} bytes", file.read); |
| 88 | let appended = written.len() - SOURCE.len(); |
| 89 | assert!( |
| 90 | file.written < appended + 2048, |
| 91 | "wrote {} bytes for {appended} appended", |
| 92 | file.written |
| 93 | ); |
| 94 | // The file has moved past the base: nothing is written, and applying needs the base. |
| 95 | let (read, written) = (file.read, file.written); |
| 96 | let error = transaction.commit(&mut file).unwrap_err(); |
| 97 | assert_eq!(error.state, CommitState::NotCommitted); |
| 98 | assert_eq!(error.error.kind(), io::ErrorKind::ResourceBusy); |
| 99 | assert_eq!(file.written, written); |
| 100 | assert!(file.read - read <= 1024); |
| 101 | assert!(transaction.apply(&mut image).is_err()); |
| 102 | } |
| 103 | |
| 104 | #[test] |
| 105 | fn stamps_name_the_committed_image() { |
| 106 | let (sid, oid) = text(SOURCE); |
| 107 | let (transaction, written) = typed(sid, oid, "x"); |
| 108 | let before = Stamp::of(SOURCE).unwrap(); |
| 109 | let after = Stamp::of(&written).unwrap(); |
| 110 | assert_ne!(before, after); |
| 111 | assert_eq!(before.length, SOURCE.len() as u64); |
| 112 | // An unpublished tail changes the length, so a commit built without it is refused. |
| 113 | let mut tail = SOURCE.to_vec(); |
| 114 | tail.extend_from_slice(&[0; 8]); |
| 115 | assert_ne!(Stamp::of(&tail).unwrap(), before); |
| 116 | let mut file = Counted { |
| 117 | bytes: tail, |
| 118 | read: 0, |
| 119 | written: 0, |
| 120 | }; |
| 121 | let error = transaction.commit(&mut file).unwrap_err(); |
| 122 | assert_eq!(error.error.kind(), io::ErrorKind::ResourceBusy); |
| 123 | assert_eq!(file.written, 0); |
| 124 | assert!(Stamp::of(&SOURCE[..1023]).is_err()); |
| 125 | } |