1use crate::{Migration, RowIdentity, SyncedTable};
4
5#[path = "followed_tables.rs"]
6mod followed_tables;
7pub use followed_tables::followed_tables_sql;
8
9pub fn path_text_sql(
13 value: rusqlite::types::ValueRef<'_>,
14) -> rusqlite::Result<rusqlite::types::Value> {
15 crate::coven_functions::path_text_sql(value)
16}
17
18#[path = "work_counts.rs"]
19mod work_counts;
20pub use work_counts::{WorkCounter, WorkCounts};
21
22pub fn report_cost(metric: &str, value: f64, unit: &str) {
27 let metric: String = metric
28 .chars()
29 .map(|c| if c.is_ascii_alphanumeric() || "_./-".contains(c) { c } else { '_' })
30 .collect();
31 eprintln!("COST metric={metric} value={value} unit={unit}");
32}
33
34pub fn report_time(metric: &str, time: std::time::Duration) {
36 report_cost(metric, time.as_secs_f64() * 1000.0, "ms");
37}
38
39pub(crate) type WriteCheckpointObserver = Box<dyn Fn(WriteCheckpoint) + Send>;
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum WriteCheckpoint {
44 Judged,
46 Committed,
48 Observed,
50 StagingReleased,
52 FileRemoved,
55 FileRemovalRecorded,
57 Finished,
59}
60
61pub struct Followed<T, E> {
64 query: crate::LiveQuery<T, E>,
65 latest: Option<T>,
66}
67
68impl<T, E> Followed<T, E>
69where
70 T: Clone + PartialEq + std::fmt::Debug + Send + 'static,
71 E: From<crate::DbError> + coven_foundation::task::CarriesBug + std::fmt::Debug + Send + 'static,
72{
73 pub fn new(query: crate::LiveQuery<T, E>) -> Self {
75 Self { query, latest: None }
76 }
77
78 pub async fn next(&mut self) -> T {
81 let next = coven_foundation::clock::within(self.query.next())
82 .await
83 .unwrap_or_else(|| panic!("no result after {:?}", self.latest))
84 .expect("the live query ended")
85 .expect("the live query failed");
86 self.latest = Some(next.clone());
87 next
88 }
89
90 pub async fn latest(&mut self) -> T {
92 match self.latest.clone() {
93 Some(latest) => latest,
94 None => self.next().await,
95 }
96 }
97
98 pub async fn wait_for(&mut self, mut matches: impl FnMut(&T) -> bool) -> T {
100 let mut result = self.latest().await;
101 while !matches(&result) {
102 result = self.next().await;
103 }
104 result
105 }
106}
107
108pub fn idle_writers() -> crate::Writers {
112 crate::Writers::new(std::collections::BTreeMap::from([(
113 coven_foundation::id_source::DeviceId(u64::MAX),
114 None,
115 )]))
116}
117
118pub fn signer() -> coven_crypto::SigningKey {
120 coven_format::test_utils::keys_from_seed(0xA5).public_keys().signing
121}
122
123pub async fn stage_plaintext(
126 database: &crate::Database,
127 id: coven_format::admin::SnapshotId,
128 prefix: coven_format::sealed_snapshot::SnapshotObjectPrefix,
129 mut input: impl std::io::Read,
130) -> Result<crate::SnapshotStaging, crate::DbError> {
131 let mut staging = database.stage_snapshot(id, prefix).await?;
132 loop {
133 let mut chunk = vec![0; coven_format::chunks::CHUNK_SIZE];
134 let length = match input.read(&mut chunk) {
135 Ok(0) => break,
136 Ok(length) => length,
137 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
138 Err(error) => return Err(crate::SnapshotError::Read(error).into()),
139 };
140 chunk.truncate(length);
141 staging.stage(chunk).await?;
142 }
143 staging.finish().await?;
144 Ok(staging)
145}
146
147pub fn notes_tables() -> Vec<SyncedTable> {
149 vec![SyncedTable::new("notes", RowIdentity::IndependentUuid)]
150}
151
152pub fn notes_migrations() -> Vec<Migration> {
154 vec![Migration::addition(1, "notes", "CREATE TABLE notes (id TEXT NOT NULL PRIMARY KEY, title TEXT NOT NULL, body TEXT NOT NULL DEFAULT '')")]
155}
156
157#[derive(Clone, Copy, Debug, Default)]
161pub struct TestAuthorship;
162
163impl TestAuthorship {
164 const STORE: coven_foundation::id_source::StoreId =
165 coven_foundation::id_source::StoreId(uuid::Uuid::from_bytes([0x10; 16]));
166
167 pub fn key() -> coven_crypto::StoreKey {
169 coven_crypto::StoreKey::from_bytes(
170 coven_foundation::id_source::KeyId(uuid::Uuid::from_bytes([1; 16])),
171 [17; 32],
172 )
173 }
174
175 fn path(write: coven_merge::WriteId) -> String {
176 coven_format::path::ObjectPath::slot(
177 Self::STORE,
178 std::num::NonZeroU64::new(write.number).expect("a positive write number"),
179 )
180 .into()
181 }
182
183 pub fn write(record: &coven_format::write::WriteRecord) -> crate::WriteAuthorship {
185 let encoder = coven_format::write_stream::WriteEncoder::new(record).unwrap();
186 Self::for_header(
187 encoder.header(),
188 record.parts.iter().map(coven_format::authorship::part_leaves).collect(),
189 )
190 }
191
192 pub(crate) fn for_header(
194 frame: &coven_format::write_stream::WriteHeaderFrame,
195 leaves: Vec<Vec<coven_format::authorship::Leaf>>,
196 ) -> crate::WriteAuthorship {
197 let key = Self::key();
198 let object = coven_format::sealed_write::WriteObjectPrefix {
199 format: coven_format::FormatVersion::CURRENT,
200 store_key: key.id(),
201 part_keys: vec![key.id(); frame.parts.len()],
202 };
203 let prefix = object.encode().unwrap();
204 let plain = frame.encode().unwrap();
205 let sealed = key
206 .derive()
207 .seal_object_chunk(&Self::path(frame.header.position), &prefix, 0, 0, &plain)
208 .unwrap();
209 let header = coven_format::sealed_write::WriteObjectLayout::new(
210 object,
211 &plain,
212 frame.parts.iter().map(|part| part.plaintext_length).collect(),
213 )
214 .unwrap()
215 .encode_chunk(&sealed)
216 .unwrap();
217 let hasher = key.derive().authorship_hasher();
218 crate::WriteAuthorship {
219 evidence: crate::HeaderEvidence {
220 prefix,
221 header,
222 roots: leaves
223 .iter()
224 .map(|leaves| coven_format::authorship::root(&hasher, leaves).unwrap())
225 .collect(),
226 signature: [0; 64],
227 },
228 hashers: frame.parts.iter().map(|_| Some(hasher.clone())).collect(),
229 }
230 }
231}
232
233impl crate::Authorship for TestAuthorship {
234 #[expect(
235 clippy::map_err_ignore,
236 reason = "a test fixture whose header does not open is the fixture refusal this names"
237 )]
238 fn header(
239 &self,
240 evidence: &coven_format::authorship::WriteEvidence,
241 ) -> Result<coven_format::write_stream::WriteHeaderFrame, crate::DbError> {
242 let sealed = coven_format::sealed_write::WriteObjectPrefix::header_chunk(&evidence.header)?;
243 let plain = Self::key()
244 .derive()
245 .open_object_chunk(&Self::path(evidence.write), &evidence.prefix, 0, 0, sealed)
246 .map_err(|_| crate::snapshot_error::invalid("test evidence header does not open"))?;
247 Ok(coven_format::write_stream::WriteHeaderFrame::decode(&plain)?)
248 }
249
250 fn hasher(
251 &self,
252 _audience: &coven_merge::Audience,
253 _key: coven_foundation::id_source::KeyId,
254 ) -> Result<coven_crypto::AuthorshipHasher, crate::DbError> {
255 Ok(Self::key().derive().authorship_hasher())
256 }
257}
258
259#[cfg(test)]
260#[path = "test_utils_tests.rs"]
261pub(crate) mod tests;