1#[path = "support/ops.rs"]
2mod ops;
3
4use onestore::{
5 CommitIo, CommitState, ExGuid, RevisionIndex, Stamp, Store, Transaction,
6 document::{Document, Kind},
7 op::{Op, PageOp},
8};
9use std::io;
10
11/// Crosses the 65535 → 65536 counter carry on its next transaction.
12const SOURCE: &[u8] =
13 include_bytes!("../../../corpus/append/round-01/tx-65535/notebook/synthetic.one");
14
15fn 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.
31fn 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.
46struct Counted {
47 bytes: Vec<u8>,
48 read: usize,
49 written: usize,
50}
51
52impl 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]
74fn 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]
105fn 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}