1use std::fs;
8use std::path::Path;
9
10use crate::cache::{CacheError, CacheHeader, decode_events};
11use crate::event::Event;
12
13#[derive(Debug, Clone)]
33pub struct CacheReader {
34 header: CacheHeader,
36 data: Vec<u8>,
38}
39
40impl CacheReader {
41 pub fn open(path: impl AsRef<Path>) -> Result<Self, CacheReaderError> {
51 let path = path.as_ref();
52 let data = fs::read(path).map_err(|e| CacheReaderError::Io {
53 path: path.display().to_string(),
54 source: e,
55 })?;
56
57 let (header, _cols) = CacheHeader::decode(&data).map_err(CacheReaderError::Cache)?;
59
60 Ok(Self { header, data })
61 }
62
63 pub fn from_bytes(data: Vec<u8>) -> Result<Self, CacheReaderError> {
69 let (header, _cols) = CacheHeader::decode(&data).map_err(CacheReaderError::Cache)?;
70 Ok(Self { header, data })
71 }
72
73 #[must_use]
75 pub fn event_count(&self) -> usize {
76 usize::try_from(self.header.row_count).unwrap_or(usize::MAX)
77 }
78
79 #[must_use]
81 pub const fn header(&self) -> &CacheHeader {
82 &self.header
83 }
84
85 pub fn read_all(&self) -> Result<Vec<Event>, CacheReaderError> {
94 let (_header, events) = decode_events(&self.data).map_err(CacheReaderError::Cache)?;
95 Ok(events)
96 }
97
98 pub fn read_range(&self, start: usize, count: usize) -> Result<Vec<Event>, CacheReaderError> {
111 if start >= self.event_count() {
112 return Ok(Vec::new());
113 }
114
115 let all = self.read_all()?;
116 let end = (start + count).min(all.len());
117 Ok(all[start..end].to_vec())
118 }
119
120 #[must_use]
122 pub const fn created_at_us(&self) -> u64 {
123 self.header.created_at_us
124 }
125
126 #[must_use]
128 pub const fn data_crc64(&self) -> u64 {
129 self.header.data_crc64
130 }
131
132 #[must_use]
134 pub const fn file_size(&self) -> usize {
135 self.data.len()
136 }
137}
138
139#[derive(Debug, thiserror::Error)]
145pub enum CacheReaderError {
146 #[error("failed to read cache file {path}: {source}")]
148 Io {
149 path: String,
150 #[source]
151 source: std::io::Error,
152 },
153
154 #[error("cache validation error: {0}")]
156 Cache(#[from] CacheError),
157}
158
159#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::cache::encode_events;
167 use crate::event::data::{CreateData, MoveData};
168 use crate::event::{Event, EventData, EventType};
169 use crate::model::item::{Kind, State, Urgency};
170 use crate::model::item_id::ItemId;
171 use std::collections::BTreeMap;
172 use tempfile::TempDir;
173
174 fn make_event(ts: i64, agent: &str, et: EventType, item: &str) -> Event {
175 let data = match et {
176 EventType::Create => EventData::Create(CreateData {
177 title: format!("Item {item}"),
178 kind: Kind::Task,
179 size: None,
180 urgency: Urgency::Default,
181 labels: vec![],
182 parent: None,
183 causation: None,
184 description: None,
185 extra: BTreeMap::new(),
186 }),
187 _ => EventData::Move(MoveData {
188 state: State::Doing,
189 reason: None,
190 extra: BTreeMap::new(),
191 }),
192 };
193 Event {
194 wall_ts_us: ts,
195 agent: agent.to_string(),
196 itc: "itc:AQ".to_string(),
197 parents: vec![],
198 event_type: et,
199 item_id: ItemId::new_unchecked(item),
200 data,
201 event_hash: format!("blake3:{ts:016x}"),
202 }
203 }
204
205 fn write_cache_file(path: &Path, events: &[Event]) {
206 let bytes = encode_events(events, 12345).unwrap();
207 std::fs::write(path, bytes).unwrap();
208 }
209
210 #[test]
213 fn open_valid_cache_file() {
214 let tmp = TempDir::new().unwrap();
215 let cache_path = tmp.path().join("events.bin");
216 let events = vec![
217 make_event(1000, "alice", EventType::Create, "bn-001"),
218 make_event(2000, "bob", EventType::Move, "bn-001"),
219 ];
220 write_cache_file(&cache_path, &events);
221
222 let reader = CacheReader::open(&cache_path).unwrap();
223 assert_eq!(reader.event_count(), 2);
224 assert_eq!(reader.created_at_us(), 12345);
225 }
226
227 #[test]
228 fn open_nonexistent_file_returns_io_error() {
229 let err = CacheReader::open("/tmp/nonexistent-bones-cache.bin").unwrap_err();
230 assert!(matches!(err, CacheReaderError::Io { .. }));
231 }
232
233 #[test]
234 fn open_corrupt_file_returns_cache_error() {
235 let tmp = TempDir::new().unwrap();
236 let cache_path = tmp.path().join("corrupt.bin");
237 std::fs::write(&cache_path, b"NOT A CACHE FILE").unwrap();
238
239 let err = CacheReader::open(&cache_path).unwrap_err();
240 assert!(matches!(err, CacheReaderError::Cache(_)));
241 }
242
243 #[test]
246 fn from_bytes_valid() {
247 let events = vec![make_event(1000, "alice", EventType::Create, "bn-001")];
248 let bytes = encode_events(&events, 42).unwrap();
249 let reader = CacheReader::from_bytes(bytes).unwrap();
250 assert_eq!(reader.event_count(), 1);
251 }
252
253 #[test]
256 fn read_all_returns_all_events() {
257 let events = vec![
258 make_event(1000, "alice", EventType::Create, "bn-001"),
259 make_event(2000, "bob", EventType::Create, "bn-002"),
260 make_event(3000, "carol", EventType::Move, "bn-001"),
261 ];
262 let bytes = encode_events(&events, 0).unwrap();
263 let reader = CacheReader::from_bytes(bytes).unwrap();
264
265 let decoded = reader.read_all().unwrap();
266 assert_eq!(decoded.len(), 3);
267 assert_eq!(decoded[0].agent, "alice");
268 assert_eq!(decoded[1].agent, "bob");
269 assert_eq!(decoded[2].event_type, EventType::Move);
270 }
271
272 #[test]
273 fn read_all_empty_cache() {
274 let bytes = encode_events(&[], 0).unwrap();
275 let reader = CacheReader::from_bytes(bytes).unwrap();
276 assert!(reader.read_all().unwrap().is_empty());
277 }
278
279 #[test]
282 fn read_range_subset() {
283 let events: Vec<Event> = (0..10)
284 .map(|i| make_event(i * 1000, "agent", EventType::Create, &format!("bn-{i:03}")))
285 .collect();
286 let bytes = encode_events(&events, 0).unwrap();
287 let reader = CacheReader::from_bytes(bytes).unwrap();
288
289 let range = reader.read_range(3, 4).unwrap();
290 assert_eq!(range.len(), 4);
291 assert_eq!(range[0].wall_ts_us, 3000);
292 assert_eq!(range[3].wall_ts_us, 6000);
293 }
294
295 #[test]
296 fn read_range_clamped_to_end() {
297 let events = vec![
298 make_event(1000, "a", EventType::Create, "bn-001"),
299 make_event(2000, "b", EventType::Create, "bn-002"),
300 ];
301 let bytes = encode_events(&events, 0).unwrap();
302 let reader = CacheReader::from_bytes(bytes).unwrap();
303
304 let range = reader.read_range(1, 100).unwrap();
306 assert_eq!(range.len(), 1);
307 assert_eq!(range[0].wall_ts_us, 2000);
308 }
309
310 #[test]
311 fn read_range_start_past_end() {
312 let events = vec![make_event(1000, "a", EventType::Create, "bn-001")];
313 let bytes = encode_events(&events, 0).unwrap();
314 let reader = CacheReader::from_bytes(bytes).unwrap();
315
316 let range = reader.read_range(5, 10).unwrap();
317 assert!(range.is_empty());
318 }
319
320 #[test]
323 fn file_size_matches_encoded_bytes() {
324 let events = vec![make_event(1000, "a", EventType::Create, "bn-001")];
325 let bytes = encode_events(&events, 0).unwrap();
326 let expected_size = bytes.len();
327 let reader = CacheReader::from_bytes(bytes).unwrap();
328 assert_eq!(reader.file_size(), expected_size);
329 }
330
331 #[test]
332 fn data_crc64_is_nonzero_for_nonempty() {
333 let events = vec![make_event(1000, "a", EventType::Create, "bn-001")];
334 let bytes = encode_events(&events, 0).unwrap();
335 let reader = CacheReader::from_bytes(bytes).unwrap();
336 assert_ne!(reader.data_crc64(), 0);
337 }
338}