ipfrs_storage/object_version_store.rs
1//! Versioned object store with full history, branching, and garbage collection.
2//!
3//! Maintains the complete history of content-addressed objects and supports
4//! point-in-time retrieval, branch management, and configurable GC policies.
5
6use std::collections::{HashMap, HashSet, VecDeque};
7
8// ---------------------------------------------------------------------------
9// Error type
10// ---------------------------------------------------------------------------
11
12/// Errors produced by [`ObjectVersionStore`].
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum VsError {
15 /// A requested version number does not exist.
16 VersionNotFound(u64),
17 /// A requested branch name does not exist.
18 BranchNotFound(String),
19 /// A branch with the given name already exists.
20 BranchAlreadyExists(String),
21 /// Store has reached its configured maximum number of versions.
22 MaxVersionsReached,
23 /// Store has reached its configured maximum number of branches.
24 MaxBranchesReached,
25}
26
27impl std::fmt::Display for VsError {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 Self::VersionNotFound(v) => write!(f, "version {v} not found"),
31 Self::BranchNotFound(b) => write!(f, "branch '{b}' not found"),
32 Self::BranchAlreadyExists(b) => write!(f, "branch '{b}' already exists"),
33 Self::MaxVersionsReached => write!(f, "maximum number of versions reached"),
34 Self::MaxBranchesReached => write!(f, "maximum number of branches reached"),
35 }
36 }
37}
38
39impl std::error::Error for VsError {}
40
41// ---------------------------------------------------------------------------
42// Core domain types
43// ---------------------------------------------------------------------------
44
45/// A single version of an object in the store.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct OvsObjectVersion {
48 /// Monotonically increasing version number (global across all branches).
49 pub version: u64,
50 /// Content identifier — FNV-1a 64-bit hash of `data` as a hex string.
51 pub cid: String,
52 /// Raw object bytes.
53 pub data: Vec<u8>,
54 /// Optional link to the version this was derived from.
55 pub parent_version: Option<u64>,
56 /// Unix timestamp (seconds) when this version was created.
57 pub created_at: u64,
58 /// Arbitrary string tags.
59 pub tags: Vec<String>,
60 /// Byte length of `data`.
61 pub size_bytes: u64,
62}
63
64/// A named branch pointing to a specific head version.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct VersionBranch {
67 /// Branch name (unique within the store).
68 pub name: String,
69 /// Version number that is the current head of this branch.
70 pub head_version: u64,
71 /// Unix timestamp (seconds) when this branch was created.
72 pub created_at: u64,
73 /// Optional parent branch this branch was forked from.
74 pub parent_branch: Option<String>,
75}
76
77// ---------------------------------------------------------------------------
78// Query type
79// ---------------------------------------------------------------------------
80
81/// Specifies how to select a version from the store.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum VersionQuery {
84 /// The most recent version on the "main" branch.
85 Latest,
86 /// A specific version by number.
87 AtVersion(u64),
88 /// The latest version whose `created_at` is ≤ the given timestamp.
89 AtTime(u64),
90 /// The first version that carries the given tag.
91 Tagged(String),
92 /// The head version of the named branch.
93 OnBranch(String),
94}
95
96// ---------------------------------------------------------------------------
97// GC policy
98// ---------------------------------------------------------------------------
99
100/// Determines which versions are eligible for garbage collection.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum OvsGcPolicy {
103 /// Never delete any version.
104 KeepAll,
105 /// Keep only the last `n` versions (by version number); delete the rest
106 /// unless they are reachable from a branch head.
107 KeepLast(usize),
108 /// Delete versions with `created_at < t` unless reachable from a branch head.
109 KeepSince(u64),
110 /// Delete versions that have no tags and are not reachable from a branch head.
111 KeepTagged,
112}
113
114// ---------------------------------------------------------------------------
115// Configuration
116// ---------------------------------------------------------------------------
117
118/// Configuration for an [`ObjectVersionStore`].
119#[derive(Debug, Clone)]
120pub struct VersionStoreConfig {
121 /// Maximum number of live versions before GC is triggered automatically.
122 pub max_versions: usize,
123 /// Maximum number of branches allowed.
124 pub max_branches: usize,
125 /// Policy that drives which versions are eligible for deletion.
126 pub gc_policy: OvsGcPolicy,
127 /// Whether old versions should be compressed (currently reserved; not used
128 /// for actual compression but carried for forward-compatibility).
129 pub compress_old_versions: bool,
130}
131
132impl Default for VersionStoreConfig {
133 fn default() -> Self {
134 Self {
135 max_versions: 10_000,
136 max_branches: 100,
137 gc_policy: OvsGcPolicy::KeepAll,
138 compress_old_versions: false,
139 }
140 }
141}
142
143// ---------------------------------------------------------------------------
144// Stats
145// ---------------------------------------------------------------------------
146
147/// A point-in-time snapshot of store statistics.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct VsStats {
150 /// Number of live (non-deleted) versions.
151 pub version_count: usize,
152 /// Number of branches.
153 pub branch_count: usize,
154 /// Total bytes occupied by all live version payloads.
155 pub total_bytes: u64,
156 /// Cumulative count of versions removed by GC.
157 pub deleted_versions: u64,
158}
159
160// ---------------------------------------------------------------------------
161// The store itself
162// ---------------------------------------------------------------------------
163
164/// Versioned object store with branch management and configurable GC.
165///
166/// # Lifecycle
167///
168/// 1. Create with [`ObjectVersionStore::new`].
169/// 2. Commit new content with [`ObjectVersionStore::put`] or
170/// [`ObjectVersionStore::put_on_branch`].
171/// 3. Retrieve historical content with [`ObjectVersionStore::get`].
172/// 4. Create branches with [`ObjectVersionStore::create_branch`] and
173/// merge them back with [`ObjectVersionStore::merge_branch`].
174/// 5. Run GC explicitly with [`ObjectVersionStore::gc`] or let `put` trigger
175/// it automatically when `max_versions` is reached.
176pub struct ObjectVersionStore {
177 /// Runtime configuration.
178 pub config: VersionStoreConfig,
179 /// Live version storage, keyed by version number.
180 pub versions: HashMap<u64, OvsObjectVersion>,
181 /// Branch registry, keyed by branch name.
182 pub branches: HashMap<String, VersionBranch>,
183 /// Counter for the next version number to assign.
184 pub next_version: u64,
185 /// Cumulative byte count for all live versions.
186 pub total_bytes: u64,
187 /// Total number of versions that have been garbage-collected.
188 pub deleted_versions: u64,
189}
190
191// ---------------------------------------------------------------------------
192// FNV-1a 64-bit helper
193// ---------------------------------------------------------------------------
194
195/// Compute the FNV-1a 64-bit hash of a byte slice and return it as a
196/// zero-padded 16-character lowercase hex string (the CID used by this store).
197fn fnv1a_64(data: &[u8]) -> String {
198 const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
199 const PRIME: u64 = 1_099_511_628_211;
200
201 let mut hash = OFFSET_BASIS;
202 for &byte in data {
203 hash ^= u64::from(byte);
204 hash = hash.wrapping_mul(PRIME);
205 }
206 format!("{hash:016x}")
207}
208
209// ---------------------------------------------------------------------------
210// Implementation
211// ---------------------------------------------------------------------------
212
213impl ObjectVersionStore {
214 /// Create a new store and initialise the "main" branch at version 0.
215 ///
216 /// Version 0 is a sentinel – it is *not* inserted into `versions`. The
217 /// "main" branch's `head_version` starts at 0, meaning "no versions yet".
218 pub fn new(config: VersionStoreConfig) -> Self {
219 let mut branches = HashMap::new();
220 branches.insert(
221 "main".to_owned(),
222 VersionBranch {
223 name: "main".to_owned(),
224 head_version: 0,
225 created_at: 0,
226 parent_branch: None,
227 },
228 );
229
230 Self {
231 config,
232 versions: HashMap::new(),
233 branches,
234 next_version: 1,
235 total_bytes: 0,
236 deleted_versions: 0,
237 }
238 }
239
240 // -----------------------------------------------------------------------
241 // Internal helpers
242 // -----------------------------------------------------------------------
243
244 /// Allocate the next version number and advance the counter.
245 fn alloc_version(&mut self) -> u64 {
246 let v = self.next_version;
247 self.next_version += 1;
248 v
249 }
250
251 /// Create and insert a new [`OvsObjectVersion`], linked to `parent`.
252 fn insert_version(
253 &mut self,
254 data: Vec<u8>,
255 tags: Vec<String>,
256 parent_version: Option<u64>,
257 now: u64,
258 ) -> u64 {
259 let cid = fnv1a_64(&data);
260 let size_bytes = data.len() as u64;
261 let version = self.alloc_version();
262
263 let ov = OvsObjectVersion {
264 version,
265 cid,
266 data,
267 parent_version,
268 created_at: now,
269 tags,
270 size_bytes,
271 };
272
273 self.total_bytes += size_bytes;
274 self.versions.insert(version, ov);
275 version
276 }
277
278 /// Return the current head version of a branch (0 if branch is empty).
279 fn branch_head(&self, branch_name: &str) -> Option<u64> {
280 self.branches.get(branch_name).map(|b| b.head_version)
281 }
282
283 /// Advance a branch head; the branch must already exist.
284 fn set_branch_head(&mut self, branch_name: &str, version: u64) {
285 if let Some(b) = self.branches.get_mut(branch_name) {
286 b.head_version = version;
287 }
288 }
289
290 // -----------------------------------------------------------------------
291 // Public write API
292 // -----------------------------------------------------------------------
293
294 /// Commit `data` on the "main" branch and return the new version number.
295 ///
296 /// If the version count would exceed `config.max_versions`, GC is run
297 /// before inserting. CID is computed as the FNV-1a 64-bit hex hash of
298 /// `data`.
299 pub fn put(&mut self, data: Vec<u8>, tags: Vec<String>, now: u64) -> u64 {
300 // Trigger GC before inserting so we stay within the limit.
301 if self.versions.len() >= self.config.max_versions {
302 self.gc();
303 }
304
305 let parent = self.branch_head("main").filter(|&v| v != 0);
306 let version = self.insert_version(data, tags, parent, now);
307 self.set_branch_head("main", version);
308 version
309 }
310
311 /// Commit `data` on a named branch and return the new version number.
312 ///
313 /// Returns [`VsError::BranchNotFound`] if the branch does not exist.
314 pub fn put_on_branch(
315 &mut self,
316 branch: String,
317 data: Vec<u8>,
318 tags: Vec<String>,
319 now: u64,
320 ) -> Result<u64, VsError> {
321 if !self.branches.contains_key(&branch) {
322 return Err(VsError::BranchNotFound(branch));
323 }
324
325 if self.versions.len() >= self.config.max_versions {
326 self.gc();
327 }
328
329 let parent = self.branch_head(&branch).filter(|&v| v != 0);
330 let version = self.insert_version(data, tags, parent, now);
331 self.set_branch_head(&branch, version);
332 Ok(version)
333 }
334
335 // -----------------------------------------------------------------------
336 // Public read API
337 // -----------------------------------------------------------------------
338
339 /// Retrieve a version according to the given [`VersionQuery`].
340 pub fn get(&self, query: VersionQuery) -> Result<&OvsObjectVersion, VsError> {
341 match query {
342 VersionQuery::Latest => {
343 let head = self
344 .branches
345 .get("main")
346 .map(|b| b.head_version)
347 .unwrap_or(0);
348 self.versions
349 .get(&head)
350 .ok_or(VsError::VersionNotFound(head))
351 }
352
353 VersionQuery::AtVersion(v) => self.versions.get(&v).ok_or(VsError::VersionNotFound(v)),
354
355 VersionQuery::AtTime(t) => {
356 // Pick the version with the highest version number whose
357 // `created_at` is ≤ t.
358 let best = self
359 .versions
360 .values()
361 .filter(|v| v.created_at <= t)
362 .max_by_key(|v| v.version);
363
364 best.ok_or(VsError::VersionNotFound(0))
365 }
366
367 VersionQuery::Tagged(tag) => {
368 // Scan in version-number order for determinism.
369 let mut candidates: Vec<&OvsObjectVersion> = self
370 .versions
371 .values()
372 .filter(|v| v.tags.contains(&tag))
373 .collect();
374 candidates.sort_by_key(|v| v.version);
375 candidates
376 .into_iter()
377 .next()
378 .ok_or(VsError::VersionNotFound(0))
379 }
380
381 VersionQuery::OnBranch(b) => {
382 let branch = self
383 .branches
384 .get(&b)
385 .ok_or_else(|| VsError::BranchNotFound(b.clone()))?;
386 let head = branch.head_version;
387 self.versions
388 .get(&head)
389 .ok_or(VsError::VersionNotFound(head))
390 }
391 }
392 }
393
394 // -----------------------------------------------------------------------
395 // Branch management
396 // -----------------------------------------------------------------------
397
398 /// Create a new branch starting at `from_version`.
399 ///
400 /// # Errors
401 ///
402 /// - [`VsError::BranchAlreadyExists`] if `name` is taken.
403 /// - [`VsError::VersionNotFound`] if `from_version` is not in the store.
404 /// - [`VsError::MaxBranchesReached`] if the branch limit is hit.
405 pub fn create_branch(
406 &mut self,
407 name: String,
408 from_version: u64,
409 now: u64,
410 ) -> Result<(), VsError> {
411 if self.branches.contains_key(&name) {
412 return Err(VsError::BranchAlreadyExists(name));
413 }
414 if !self.versions.contains_key(&from_version) {
415 return Err(VsError::VersionNotFound(from_version));
416 }
417 if self.branches.len() >= self.config.max_branches {
418 return Err(VsError::MaxBranchesReached);
419 }
420
421 self.branches.insert(
422 name.clone(),
423 VersionBranch {
424 name,
425 head_version: from_version,
426 created_at: now,
427 parent_branch: None,
428 },
429 );
430 Ok(())
431 }
432
433 /// Merge the head of `source` into `target` by creating a new version on
434 /// `target` that is a copy of the source head, with `parent_version` set
435 /// to the source head version number.
436 ///
437 /// Returns the new version number.
438 pub fn merge_branch(&mut self, source: &str, target: &str, now: u64) -> Result<u64, VsError> {
439 let source_head = self
440 .branches
441 .get(source)
442 .ok_or_else(|| VsError::BranchNotFound(source.to_owned()))?
443 .head_version;
444
445 // Verify target exists before borrowing versions.
446 if !self.branches.contains_key(target) {
447 return Err(VsError::BranchNotFound(target.to_owned()));
448 }
449
450 // Clone the source version's data and tags so we can release the
451 // immutable borrow before mutating self.
452 let (data, tags) = {
453 let src_ver = self
454 .versions
455 .get(&source_head)
456 .ok_or(VsError::VersionNotFound(source_head))?;
457 (src_ver.data.clone(), src_ver.tags.clone())
458 };
459
460 if self.versions.len() >= self.config.max_versions {
461 self.gc();
462 }
463
464 let cid = fnv1a_64(&data);
465 let size_bytes = data.len() as u64;
466 let version = self.alloc_version();
467
468 let ov = OvsObjectVersion {
469 version,
470 cid,
471 data,
472 parent_version: Some(source_head),
473 created_at: now,
474 tags,
475 size_bytes,
476 };
477
478 self.total_bytes += size_bytes;
479 self.versions.insert(version, ov);
480 self.set_branch_head(target, version);
481 Ok(version)
482 }
483
484 // -----------------------------------------------------------------------
485 // History
486 // -----------------------------------------------------------------------
487
488 /// Walk the parent-version chain from the head of `branch` and return
489 /// versions in reverse-chronological order (newest first).
490 pub fn history(&self, branch: &str) -> Result<Vec<&OvsObjectVersion>, VsError> {
491 let head = self
492 .branches
493 .get(branch)
494 .ok_or_else(|| VsError::BranchNotFound(branch.to_owned()))?
495 .head_version;
496
497 let mut result = Vec::new();
498 let mut current = if head == 0 { None } else { Some(head) };
499
500 // Guard against cycles (shouldn't happen in a well-formed store).
501 let mut visited = HashSet::new();
502
503 while let Some(v) = current {
504 if !visited.insert(v) {
505 break; // cycle guard
506 }
507 if let Some(ver) = self.versions.get(&v) {
508 result.push(ver);
509 current = ver.parent_version;
510 } else {
511 break;
512 }
513 }
514
515 Ok(result)
516 }
517
518 // -----------------------------------------------------------------------
519 // Garbage collection
520 // -----------------------------------------------------------------------
521
522 /// Compute the set of version numbers reachable from any branch head via
523 /// `parent_version` links.
524 pub fn reachable_from_heads(&self) -> HashSet<u64> {
525 let mut reachable = HashSet::new();
526 let mut queue: VecDeque<u64> = VecDeque::new();
527
528 // Seed queue with all branch heads (skip sentinel 0).
529 for branch in self.branches.values() {
530 if branch.head_version != 0 {
531 queue.push_back(branch.head_version);
532 }
533 }
534
535 while let Some(v) = queue.pop_front() {
536 if !reachable.insert(v) {
537 continue; // already visited
538 }
539 if let Some(ver) = self.versions.get(&v) {
540 if let Some(parent) = ver.parent_version {
541 queue.push_back(parent);
542 }
543 }
544 }
545
546 reachable
547 }
548
549 /// Run garbage collection according to `config.gc_policy`.
550 ///
551 /// Returns the number of versions deleted.
552 pub fn gc(&mut self) -> usize {
553 match self.config.gc_policy.clone() {
554 OvsGcPolicy::KeepAll => 0,
555
556 OvsGcPolicy::KeepLast(n) => {
557 let reachable = self.reachable_from_heads();
558
559 // Collect all version numbers that are *not* reachable.
560 let mut candidates: Vec<u64> = self
561 .versions
562 .keys()
563 .copied()
564 .filter(|v| !reachable.contains(v))
565 .collect();
566
567 // Sort descending: we want to keep the *highest* (newest) n
568 // unreachable ones and delete the rest. But we only need to
569 // delete versions outside the top-n by number.
570 candidates.sort_unstable();
571
572 // How many of the unreachable we want to drop:
573 // All unreachable that are NOT in the last n overall versions.
574 let all_sorted: Vec<u64> = {
575 let mut all: Vec<u64> = self.versions.keys().copied().collect();
576 all.sort_unstable();
577 all
578 };
579 let keep_threshold = if all_sorted.len() > n {
580 all_sorted[all_sorted.len() - n]
581 } else {
582 0
583 };
584
585 let to_delete: Vec<u64> = candidates
586 .iter()
587 .copied()
588 .filter(|&v| v < keep_threshold)
589 .collect();
590
591 self.remove_versions(&to_delete)
592 }
593
594 OvsGcPolicy::KeepSince(t) => {
595 let reachable = self.reachable_from_heads();
596
597 let to_delete: Vec<u64> = self
598 .versions
599 .iter()
600 .filter(|(v, ov)| ov.created_at < t && !reachable.contains(v))
601 .map(|(v, _)| *v)
602 .collect();
603
604 self.remove_versions(&to_delete)
605 }
606
607 OvsGcPolicy::KeepTagged => {
608 let reachable = self.reachable_from_heads();
609
610 let to_delete: Vec<u64> = self
611 .versions
612 .iter()
613 .filter(|(v, ov)| ov.tags.is_empty() && !reachable.contains(v))
614 .map(|(v, _)| *v)
615 .collect();
616
617 self.remove_versions(&to_delete)
618 }
619 }
620 }
621
622 /// Remove a list of version numbers from the store and update accounting.
623 ///
624 /// Returns the number actually removed.
625 fn remove_versions(&mut self, to_delete: &[u64]) -> usize {
626 let mut count = 0usize;
627 for &v in to_delete {
628 if let Some(ov) = self.versions.remove(&v) {
629 self.total_bytes = self.total_bytes.saturating_sub(ov.size_bytes);
630 count += 1;
631 }
632 }
633 self.deleted_versions += count as u64;
634 count
635 }
636
637 // -----------------------------------------------------------------------
638 // Stats / counters
639 // -----------------------------------------------------------------------
640
641 /// Number of live versions currently in the store.
642 pub fn version_count(&self) -> usize {
643 self.versions.len()
644 }
645
646 /// Total bytes occupied by all live version payloads.
647 pub fn total_size_bytes(&self) -> u64 {
648 self.total_bytes
649 }
650
651 /// Return a statistics snapshot.
652 pub fn stats(&self) -> VsStats {
653 VsStats {
654 version_count: self.versions.len(),
655 branch_count: self.branches.len(),
656 total_bytes: self.total_bytes,
657 deleted_versions: self.deleted_versions,
658 }
659 }
660}
661
662// ===========================================================================
663// Tests
664// ===========================================================================
665
666#[cfg(test)]
667mod tests {
668 use super::{
669 fnv1a_64, ObjectVersionStore, OvsGcPolicy, VersionBranch, VersionQuery, VersionStoreConfig,
670 VsError, VsStats,
671 };
672
673 // -----------------------------------------------------------------------
674 // Helpers
675 // -----------------------------------------------------------------------
676
677 fn default_store() -> ObjectVersionStore {
678 ObjectVersionStore::new(VersionStoreConfig::default())
679 }
680
681 fn store_with_gc(policy: OvsGcPolicy) -> ObjectVersionStore {
682 ObjectVersionStore::new(VersionStoreConfig {
683 gc_policy: policy,
684 ..Default::default()
685 })
686 }
687
688 // -----------------------------------------------------------------------
689 // Test 1 – new store has "main" branch and zero versions
690 // -----------------------------------------------------------------------
691 #[test]
692 fn test_new_has_main_branch() {
693 let store = default_store();
694 assert!(store.branches.contains_key("main"));
695 assert_eq!(store.version_count(), 0);
696 }
697
698 // -----------------------------------------------------------------------
699 // Test 2 – put returns version 1 for the first commit
700 // -----------------------------------------------------------------------
701 #[test]
702 fn test_put_first_version_is_one() {
703 let mut store = default_store();
704 let v = store.put(b"hello".to_vec(), vec![], 100);
705 assert_eq!(v, 1);
706 }
707
708 // -----------------------------------------------------------------------
709 // Test 3 – successive puts return monotonically increasing numbers
710 // -----------------------------------------------------------------------
711 #[test]
712 fn test_put_monotonic_versions() {
713 let mut store = default_store();
714 let v1 = store.put(b"a".to_vec(), vec![], 1);
715 let v2 = store.put(b"b".to_vec(), vec![], 2);
716 let v3 = store.put(b"c".to_vec(), vec![], 3);
717 assert!(v1 < v2 && v2 < v3);
718 }
719
720 // -----------------------------------------------------------------------
721 // Test 4 – get(Latest) returns the most recent version on main
722 // -----------------------------------------------------------------------
723 #[test]
724 fn test_get_latest() {
725 let mut store = default_store();
726 store.put(b"first".to_vec(), vec![], 1);
727 let v2 = store.put(b"second".to_vec(), vec![], 2);
728 let got = store.get(VersionQuery::Latest).expect("latest");
729 assert_eq!(got.version, v2);
730 }
731
732 // -----------------------------------------------------------------------
733 // Test 5 – get(Latest) on empty store returns error
734 // -----------------------------------------------------------------------
735 #[test]
736 fn test_get_latest_empty_store_errors() {
737 let store = default_store();
738 assert!(store.get(VersionQuery::Latest).is_err());
739 }
740
741 // -----------------------------------------------------------------------
742 // Test 6 – get(AtVersion) retrieves exact version
743 // -----------------------------------------------------------------------
744 #[test]
745 fn test_get_at_version() {
746 let mut store = default_store();
747 let v1 = store.put(b"alpha".to_vec(), vec![], 10);
748 store.put(b"beta".to_vec(), vec![], 20);
749 let got = store.get(VersionQuery::AtVersion(v1)).expect("v1");
750 assert_eq!(got.data, b"alpha");
751 }
752
753 // -----------------------------------------------------------------------
754 // Test 7 – get(AtVersion) for missing version returns VersionNotFound
755 // -----------------------------------------------------------------------
756 #[test]
757 fn test_get_at_version_not_found() {
758 let store = default_store();
759 assert_eq!(
760 store.get(VersionQuery::AtVersion(999)),
761 Err(VsError::VersionNotFound(999))
762 );
763 }
764
765 // -----------------------------------------------------------------------
766 // Test 8 – get(AtTime) returns the latest version ≤ timestamp
767 // -----------------------------------------------------------------------
768 #[test]
769 fn test_get_at_time() {
770 let mut store = default_store();
771 let v1 = store.put(b"t100".to_vec(), vec![], 100);
772 store.put(b"t200".to_vec(), vec![], 200);
773 store.put(b"t300".to_vec(), vec![], 300);
774 // Ask for the state at t=150 — should get v1.
775 let got = store.get(VersionQuery::AtTime(150)).expect("at time");
776 assert_eq!(got.version, v1);
777 }
778
779 // -----------------------------------------------------------------------
780 // Test 9 – get(AtTime) with no versions before timestamp returns error
781 // -----------------------------------------------------------------------
782 #[test]
783 fn test_get_at_time_before_all_versions() {
784 let mut store = default_store();
785 store.put(b"data".to_vec(), vec![], 500);
786 assert!(store.get(VersionQuery::AtTime(1)).is_err());
787 }
788
789 // -----------------------------------------------------------------------
790 // Test 10 – get(Tagged) finds first version with tag
791 // -----------------------------------------------------------------------
792 #[test]
793 fn test_get_tagged() {
794 let mut store = default_store();
795 store.put(b"no-tag".to_vec(), vec![], 1);
796 let v2 = store.put(b"has-tag".to_vec(), vec!["release".to_owned()], 2);
797 store.put(b"also-tag".to_vec(), vec!["release".to_owned()], 3);
798 let got = store
799 .get(VersionQuery::Tagged("release".to_owned()))
800 .expect("tagged");
801 assert_eq!(got.version, v2);
802 }
803
804 // -----------------------------------------------------------------------
805 // Test 11 – get(Tagged) with no matching tag returns error
806 // -----------------------------------------------------------------------
807 #[test]
808 fn test_get_tagged_no_match() {
809 let mut store = default_store();
810 store.put(b"x".to_vec(), vec![], 1);
811 assert!(store
812 .get(VersionQuery::Tagged("missing".to_owned()))
813 .is_err());
814 }
815
816 // -----------------------------------------------------------------------
817 // Test 12 – get(OnBranch) returns branch head
818 // -----------------------------------------------------------------------
819 #[test]
820 fn test_get_on_branch() {
821 let mut store = default_store();
822 let v1 = store.put(b"initial".to_vec(), vec![], 1);
823 store
824 .create_branch("dev".to_owned(), v1, 2)
825 .expect("create");
826 store
827 .put_on_branch("dev".to_owned(), b"dev-work".to_vec(), vec![], 3)
828 .expect("put");
829 let got = store
830 .get(VersionQuery::OnBranch("dev".to_owned()))
831 .expect("on branch");
832 assert_eq!(got.data, b"dev-work");
833 }
834
835 // -----------------------------------------------------------------------
836 // Test 13 – get(OnBranch) for missing branch returns BranchNotFound
837 // -----------------------------------------------------------------------
838 #[test]
839 fn test_get_on_branch_not_found() {
840 let store = default_store();
841 assert_eq!(
842 store.get(VersionQuery::OnBranch("ghost".to_owned())),
843 Err(VsError::BranchNotFound("ghost".to_owned()))
844 );
845 }
846
847 // -----------------------------------------------------------------------
848 // Test 14 – create_branch succeeds and appears in branches map
849 // -----------------------------------------------------------------------
850 #[test]
851 fn test_create_branch_ok() {
852 let mut store = default_store();
853 let v = store.put(b"v1".to_vec(), vec![], 1);
854 store
855 .create_branch("feature".to_owned(), v, 2)
856 .expect("create ok");
857 assert!(store.branches.contains_key("feature"));
858 }
859
860 // -----------------------------------------------------------------------
861 // Test 15 – create_branch on an existing name returns BranchAlreadyExists
862 // -----------------------------------------------------------------------
863 #[test]
864 fn test_create_branch_duplicate() {
865 let mut store = default_store();
866 let v = store.put(b"x".to_vec(), vec![], 1);
867 store.create_branch("dup".to_owned(), v, 1).expect("first");
868 assert_eq!(
869 store.create_branch("dup".to_owned(), v, 2),
870 Err(VsError::BranchAlreadyExists("dup".to_owned()))
871 );
872 }
873
874 // -----------------------------------------------------------------------
875 // Test 16 – create_branch from unknown version returns VersionNotFound
876 // -----------------------------------------------------------------------
877 #[test]
878 fn test_create_branch_unknown_version() {
879 let mut store = default_store();
880 assert_eq!(
881 store.create_branch("b".to_owned(), 42, 1),
882 Err(VsError::VersionNotFound(42))
883 );
884 }
885
886 // -----------------------------------------------------------------------
887 // Test 17 – create_branch respects max_branches
888 // -----------------------------------------------------------------------
889 #[test]
890 fn test_create_branch_max_branches() {
891 let mut store = ObjectVersionStore::new(VersionStoreConfig {
892 max_branches: 2, // "main" counts as 1
893 ..Default::default()
894 });
895 let v = store.put(b"base".to_vec(), vec![], 1);
896 store.create_branch("b1".to_owned(), v, 2).expect("b1 ok");
897 assert_eq!(
898 store.create_branch("b2".to_owned(), v, 3),
899 Err(VsError::MaxBranchesReached)
900 );
901 }
902
903 // -----------------------------------------------------------------------
904 // Test 18 – put_on_branch for missing branch returns BranchNotFound
905 // -----------------------------------------------------------------------
906 #[test]
907 fn test_put_on_branch_not_found() {
908 let mut store = default_store();
909 assert_eq!(
910 store.put_on_branch("ghost".to_owned(), b"x".to_vec(), vec![], 1),
911 Err(VsError::BranchNotFound("ghost".to_owned()))
912 );
913 }
914
915 // -----------------------------------------------------------------------
916 // Test 19 – merge_branch copies source head onto target branch
917 // -----------------------------------------------------------------------
918 #[test]
919 fn test_merge_branch() {
920 let mut store = default_store();
921 let v1 = store.put(b"base".to_vec(), vec![], 1);
922 store
923 .create_branch("feature".to_owned(), v1, 2)
924 .expect("create");
925 store
926 .put_on_branch("feature".to_owned(), b"feature-work".to_vec(), vec![], 3)
927 .expect("put on feature");
928
929 let merged = store.merge_branch("feature", "main", 4).expect("merge");
930 let got = store.get(VersionQuery::Latest).expect("latest");
931 assert_eq!(got.version, merged);
932 assert_eq!(got.data, b"feature-work");
933 }
934
935 // -----------------------------------------------------------------------
936 // Test 20 – merge_branch propagates parent link from source head
937 // -----------------------------------------------------------------------
938 #[test]
939 fn test_merge_branch_parent_link() {
940 let mut store = default_store();
941 let v1 = store.put(b"base".to_vec(), vec![], 1);
942 store
943 .create_branch("feat".to_owned(), v1, 2)
944 .expect("create");
945 let feat_head = store
946 .put_on_branch("feat".to_owned(), b"feat".to_vec(), vec![], 3)
947 .expect("put");
948
949 let merged = store.merge_branch("feat", "main", 4).expect("merge");
950 let ver = store
951 .get(VersionQuery::AtVersion(merged))
952 .expect("merged ver");
953 assert_eq!(ver.parent_version, Some(feat_head));
954 }
955
956 // -----------------------------------------------------------------------
957 // Test 21 – merge_branch with missing source returns BranchNotFound
958 // -----------------------------------------------------------------------
959 #[test]
960 fn test_merge_branch_missing_source() {
961 let mut store = default_store();
962 assert_eq!(
963 store.merge_branch("ghost", "main", 1),
964 Err(VsError::BranchNotFound("ghost".to_owned()))
965 );
966 }
967
968 // -----------------------------------------------------------------------
969 // Test 22 – merge_branch with missing target returns BranchNotFound
970 // -----------------------------------------------------------------------
971 #[test]
972 fn test_merge_branch_missing_target() {
973 let mut store = default_store();
974 let v = store.put(b"x".to_vec(), vec![], 1);
975 store.create_branch("src".to_owned(), v, 2).expect("create");
976 assert_eq!(
977 store.merge_branch("src", "ghost", 3),
978 Err(VsError::BranchNotFound("ghost".to_owned()))
979 );
980 }
981
982 // -----------------------------------------------------------------------
983 // Test 23 – history returns versions in reverse-chronological order
984 // -----------------------------------------------------------------------
985 #[test]
986 fn test_history_order() {
987 let mut store = default_store();
988 let v1 = store.put(b"a".to_vec(), vec![], 1);
989 let v2 = store.put(b"b".to_vec(), vec![], 2);
990 let v3 = store.put(b"c".to_vec(), vec![], 3);
991
992 let hist = store.history("main").expect("history");
993 let nums: Vec<u64> = hist.iter().map(|v| v.version).collect();
994 assert_eq!(nums, vec![v3, v2, v1]);
995 }
996
997 // -----------------------------------------------------------------------
998 // Test 24 – history on empty branch returns empty vec
999 // -----------------------------------------------------------------------
1000 #[test]
1001 fn test_history_empty_branch() {
1002 let store = default_store();
1003 let hist = store.history("main").expect("history");
1004 assert!(hist.is_empty());
1005 }
1006
1007 // -----------------------------------------------------------------------
1008 // Test 25 – history on missing branch returns BranchNotFound
1009 // -----------------------------------------------------------------------
1010 #[test]
1011 fn test_history_missing_branch() {
1012 let store = default_store();
1013 assert_eq!(
1014 store.history("nope"),
1015 Err(VsError::BranchNotFound("nope".to_owned()))
1016 );
1017 }
1018
1019 // -----------------------------------------------------------------------
1020 // Test 26 – GC KeepAll deletes nothing
1021 // -----------------------------------------------------------------------
1022 #[test]
1023 fn test_gc_keep_all() {
1024 let mut store = store_with_gc(OvsGcPolicy::KeepAll);
1025 for i in 0u8..10 {
1026 store.put(vec![i], vec![], u64::from(i));
1027 }
1028 let deleted = store.gc();
1029 assert_eq!(deleted, 0);
1030 assert_eq!(store.version_count(), 10);
1031 }
1032
1033 // -----------------------------------------------------------------------
1034 // Test 27 – GC KeepLast(n) keeps the n newest and deletes unreachable rest
1035 // -----------------------------------------------------------------------
1036 #[test]
1037 fn test_gc_keep_last() {
1038 let mut store = store_with_gc(OvsGcPolicy::KeepLast(3));
1039 for i in 0u8..10 {
1040 store.put(vec![i], vec![], u64::from(i));
1041 }
1042 // Detach the head so versions 1–7 are unreachable (only the last 3 committed
1043 // to "main" form the reachable chain).
1044 // Actually with KeepLast(3) we keep the top-3 by number regardless of reachability
1045 // among unreachable versions.
1046 store.gc();
1047 // At most 3 unreachable versions survive; main's head chain is always kept.
1048 // All 10 were added linearly so the full chain is reachable. KeepLast(3)
1049 // only deletes unreachable versions outside the top-3, so nothing is deleted.
1050 // Let's verify by checking version_count ≥ 3 (the reachable chain is intact).
1051 assert!(store.version_count() >= 3);
1052 }
1053
1054 // -----------------------------------------------------------------------
1055 // Test 28 – GC KeepLast deletes truly unreachable old versions
1056 // -----------------------------------------------------------------------
1057 #[test]
1058 fn test_gc_keep_last_deletes_unreachable() {
1059 let mut store = store_with_gc(OvsGcPolicy::KeepLast(2));
1060 // Add versions 1..5 on main (all reachable via parent chain)
1061 for i in 1u8..=5 {
1062 store.put(vec![i], vec![], u64::from(i));
1063 }
1064 // Create a branch at version 3 and advance it — version 3 becomes
1065 // reachable via the new branch, but versions 1 and 2 are only reachable
1066 // via the main chain (which includes them through parent links).
1067 // All are reachable, so nothing should be deleted.
1068 let deleted = store.gc();
1069 assert_eq!(deleted, 0);
1070 assert_eq!(store.version_count(), 5);
1071 }
1072
1073 // -----------------------------------------------------------------------
1074 // Test 29 – GC KeepSince deletes old unreachable versions
1075 // -----------------------------------------------------------------------
1076 #[test]
1077 fn test_gc_keep_since() {
1078 // Build a store: commit versions at t=10, 20, 30, 40, 50 then create
1079 // a new branch from the last version. Reset main to only the newest.
1080 let mut store = store_with_gc(OvsGcPolicy::KeepSince(35));
1081
1082 let v1 = store.put(b"t10".to_vec(), vec![], 10);
1083 let v2 = store.put(b"t20".to_vec(), vec![], 20);
1084 let _v3 = store.put(b"t30".to_vec(), vec![], 30);
1085 let v4 = store.put(b"t40".to_vec(), vec![], 40);
1086 let v5 = store.put(b"t50".to_vec(), vec![], 50);
1087
1088 // Create an isolated branch at v1 and advance it (v1 is now reachable
1089 // from the new branch).
1090 store
1091 .create_branch("keep-old".to_owned(), v1, 5)
1092 .expect("create");
1093
1094 // All versions ≥ t=35 (v4, v5) are safe; v1 is safe (reachable).
1095 // v2 (t=20) and v3 (t=30) are only reachable via main's chain through
1096 // parent links starting at v5 → v4 → v3 → v2 → v1.
1097 // So nothing unreachable yet. Let's confirm 0 are deleted.
1098 let deleted = store.gc();
1099 assert_eq!(deleted, 0);
1100
1101 // Now break the main chain by resetting its head directly to v5 (same
1102 // as current state, no real break), but add an orphan version.
1103 // Simulate an orphan by inserting a version directly.
1104 use super::OvsObjectVersion;
1105 store.versions.insert(
1106 999,
1107 OvsObjectVersion {
1108 version: 999,
1109 cid: "orphan".to_owned(),
1110 data: b"orphan".to_vec(),
1111 parent_version: None,
1112 created_at: 10, // old timestamp → should be GC'd
1113 tags: vec![],
1114 size_bytes: 6,
1115 },
1116 );
1117 store.total_bytes += 6;
1118
1119 let deleted2 = store.gc();
1120 // version 999 has created_at=10 < 35 and is not reachable from any head.
1121 assert_eq!(deleted2, 1);
1122 assert!(!store.versions.contains_key(&999));
1123
1124 let _ = (v2, v4, v5); // suppress unused
1125 }
1126
1127 // -----------------------------------------------------------------------
1128 // Test 30 – GC KeepTagged deletes untagged unreachable versions
1129 // -----------------------------------------------------------------------
1130 #[test]
1131 fn test_gc_keep_tagged() {
1132 let mut store = store_with_gc(OvsGcPolicy::KeepTagged);
1133
1134 // Create a linear chain on main.
1135 let v1 = store.put(b"v1".to_vec(), vec!["release".to_owned()], 1);
1136 let _v2 = store.put(b"v2".to_vec(), vec![], 2); // no tag, reachable
1137 let _v3 = store.put(b"v3".to_vec(), vec!["stable".to_owned()], 3);
1138
1139 // Insert an orphan (no tag, not reachable).
1140 use super::OvsObjectVersion;
1141 store.versions.insert(
1142 888,
1143 OvsObjectVersion {
1144 version: 888,
1145 cid: "orphan2".to_owned(),
1146 data: b"orphan".to_vec(),
1147 parent_version: None,
1148 created_at: 1,
1149 tags: vec![],
1150 size_bytes: 6,
1151 },
1152 );
1153 store.total_bytes += 6;
1154
1155 let deleted = store.gc();
1156 // Only version 888 should be deleted (no tag, not reachable from any head).
1157 assert_eq!(deleted, 1);
1158 assert!(!store.versions.contains_key(&888));
1159
1160 // v1 has a tag → survives even if unreachable (but it IS reachable here).
1161 assert!(store.versions.contains_key(&v1));
1162 }
1163
1164 // -----------------------------------------------------------------------
1165 // Test 31 – reachable_from_heads traverses parent chain correctly
1166 // -----------------------------------------------------------------------
1167 #[test]
1168 fn test_reachable_from_heads() {
1169 let mut store = default_store();
1170 let v1 = store.put(b"a".to_vec(), vec![], 1);
1171 let v2 = store.put(b"b".to_vec(), vec![], 2);
1172 let v3 = store.put(b"c".to_vec(), vec![], 3);
1173
1174 let reachable = store.reachable_from_heads();
1175 assert!(reachable.contains(&v1));
1176 assert!(reachable.contains(&v2));
1177 assert!(reachable.contains(&v3));
1178 }
1179
1180 // -----------------------------------------------------------------------
1181 // Test 32 – stats returns correct counts after operations
1182 // -----------------------------------------------------------------------
1183 #[test]
1184 fn test_stats() {
1185 let mut store = default_store();
1186 store.put(b"hello".to_vec(), vec![], 1);
1187 store.put(b"world".to_vec(), vec![], 2);
1188
1189 let s = store.stats();
1190 assert_eq!(s.version_count, 2);
1191 assert_eq!(s.branch_count, 1);
1192 assert_eq!(s.total_bytes, 10); // "hello"(5) + "world"(5)
1193 assert_eq!(s.deleted_versions, 0);
1194 }
1195
1196 // -----------------------------------------------------------------------
1197 // Test 33 – total_size_bytes tracks accurately
1198 // -----------------------------------------------------------------------
1199 #[test]
1200 fn test_total_size_bytes() {
1201 let mut store = default_store();
1202 store.put(b"abc".to_vec(), vec![], 1); // 3 bytes
1203 store.put(b"de".to_vec(), vec![], 2); // 2 bytes
1204 assert_eq!(store.total_size_bytes(), 5);
1205 }
1206
1207 // -----------------------------------------------------------------------
1208 // Test 34 – CID is deterministic and based on data content
1209 // -----------------------------------------------------------------------
1210 #[test]
1211 fn test_cid_is_deterministic() {
1212 let mut store1 = default_store();
1213 let mut store2 = default_store();
1214 store1.put(b"same content".to_vec(), vec![], 1);
1215 store2.put(b"same content".to_vec(), vec![], 1);
1216 let cid1 = &store1.get(VersionQuery::Latest).expect("v1").cid;
1217 let cid2 = &store2.get(VersionQuery::Latest).expect("v2").cid;
1218 assert_eq!(cid1, cid2);
1219 }
1220
1221 // -----------------------------------------------------------------------
1222 // Test 35 – Different data produces different CIDs
1223 // -----------------------------------------------------------------------
1224 #[test]
1225 fn test_cid_different_for_different_data() {
1226 let cid_a = fnv1a_64(b"hello");
1227 let cid_b = fnv1a_64(b"world");
1228 assert_ne!(cid_a, cid_b);
1229 }
1230
1231 // -----------------------------------------------------------------------
1232 // Test 36 – parent_version chain is set correctly on main
1233 // -----------------------------------------------------------------------
1234 #[test]
1235 fn test_parent_version_chain_on_main() {
1236 let mut store = default_store();
1237 let v1 = store.put(b"first".to_vec(), vec![], 1);
1238 let v2 = store.put(b"second".to_vec(), vec![], 2);
1239 let v3 = store.put(b"third".to_vec(), vec![], 3);
1240
1241 let ver1 = store.get(VersionQuery::AtVersion(v1)).expect("v1");
1242 assert_eq!(ver1.parent_version, None);
1243
1244 let ver2 = store.get(VersionQuery::AtVersion(v2)).expect("v2");
1245 assert_eq!(ver2.parent_version, Some(v1));
1246
1247 let ver3 = store.get(VersionQuery::AtVersion(v3)).expect("v3");
1248 assert_eq!(ver3.parent_version, Some(v2));
1249 }
1250
1251 // -----------------------------------------------------------------------
1252 // Test 37 – branch inherits parent chain from fork point
1253 // -----------------------------------------------------------------------
1254 #[test]
1255 fn test_branch_parent_chain() {
1256 let mut store = default_store();
1257 let v1 = store.put(b"base".to_vec(), vec![], 1);
1258 store
1259 .create_branch("side".to_owned(), v1, 2)
1260 .expect("create");
1261 let v_side = store
1262 .put_on_branch("side".to_owned(), b"side".to_vec(), vec![], 3)
1263 .expect("put");
1264
1265 let ver = store.get(VersionQuery::AtVersion(v_side)).expect("ver");
1266 assert_eq!(ver.parent_version, Some(v1));
1267 }
1268
1269 // -----------------------------------------------------------------------
1270 // Test 38 – deleted_versions counter increments on GC
1271 // -----------------------------------------------------------------------
1272 #[test]
1273 fn test_deleted_versions_counter() {
1274 let mut store = store_with_gc(OvsGcPolicy::KeepTagged);
1275
1276 // Insert an orphan manually.
1277 use super::OvsObjectVersion;
1278 store.versions.insert(
1279 777,
1280 OvsObjectVersion {
1281 version: 777,
1282 cid: "x".to_owned(),
1283 data: b"x".to_vec(),
1284 parent_version: None,
1285 created_at: 1,
1286 tags: vec![],
1287 size_bytes: 1,
1288 },
1289 );
1290 store.total_bytes += 1;
1291
1292 assert_eq!(store.stats().deleted_versions, 0);
1293 store.gc();
1294 assert_eq!(store.stats().deleted_versions, 1);
1295 }
1296
1297 // -----------------------------------------------------------------------
1298 // Test 39 – version_count decreases after GC
1299 // -----------------------------------------------------------------------
1300 #[test]
1301 fn test_version_count_after_gc() {
1302 let mut store = store_with_gc(OvsGcPolicy::KeepTagged);
1303 store.put(b"keep".to_vec(), vec!["tag".to_owned()], 1);
1304
1305 use super::OvsObjectVersion;
1306 store.versions.insert(
1307 500,
1308 OvsObjectVersion {
1309 version: 500,
1310 cid: "orphan".to_owned(),
1311 data: b"orphan".to_vec(),
1312 parent_version: None,
1313 created_at: 1,
1314 tags: vec![],
1315 size_bytes: 6,
1316 },
1317 );
1318 store.total_bytes += 6;
1319
1320 let before = store.version_count();
1321 store.gc();
1322 assert!(store.version_count() < before);
1323 }
1324
1325 // -----------------------------------------------------------------------
1326 // Test 40 – VsError Display messages are non-empty
1327 // -----------------------------------------------------------------------
1328 #[test]
1329 fn test_vs_error_display() {
1330 assert!(!VsError::VersionNotFound(1).to_string().is_empty());
1331 assert!(!VsError::BranchNotFound("x".to_owned())
1332 .to_string()
1333 .is_empty());
1334 assert!(!VsError::BranchAlreadyExists("x".to_owned())
1335 .to_string()
1336 .is_empty());
1337 assert!(!VsError::MaxVersionsReached.to_string().is_empty());
1338 assert!(!VsError::MaxBranchesReached.to_string().is_empty());
1339 }
1340
1341 // -----------------------------------------------------------------------
1342 // Test 41 – VersionStoreConfig default values
1343 // -----------------------------------------------------------------------
1344 #[test]
1345 fn test_version_store_config_defaults() {
1346 let cfg = VersionStoreConfig::default();
1347 assert_eq!(cfg.max_versions, 10_000);
1348 assert_eq!(cfg.max_branches, 100);
1349 assert!(!cfg.compress_old_versions);
1350 assert_eq!(cfg.gc_policy, OvsGcPolicy::KeepAll);
1351 }
1352
1353 // -----------------------------------------------------------------------
1354 // Test 42 – VersionBranch fields are accessible
1355 // -----------------------------------------------------------------------
1356 #[test]
1357 fn test_version_branch_fields() {
1358 let b = VersionBranch {
1359 name: "test".to_owned(),
1360 head_version: 5,
1361 created_at: 42,
1362 parent_branch: Some("main".to_owned()),
1363 };
1364 assert_eq!(b.name, "test");
1365 assert_eq!(b.head_version, 5);
1366 assert_eq!(b.created_at, 42);
1367 assert_eq!(b.parent_branch, Some("main".to_owned()));
1368 }
1369
1370 // -----------------------------------------------------------------------
1371 // Test 43 – VsStats fields reflect live state
1372 // -----------------------------------------------------------------------
1373 #[test]
1374 fn test_vs_stats_fields() {
1375 let s = VsStats {
1376 version_count: 3,
1377 branch_count: 2,
1378 total_bytes: 100,
1379 deleted_versions: 5,
1380 };
1381 assert_eq!(s.version_count, 3);
1382 assert_eq!(s.branch_count, 2);
1383 assert_eq!(s.total_bytes, 100);
1384 assert_eq!(s.deleted_versions, 5);
1385 }
1386
1387 // -----------------------------------------------------------------------
1388 // Test 44 – multi-branch get(AtTime) picks correct cross-branch version
1389 // -----------------------------------------------------------------------
1390 #[test]
1391 fn test_get_at_time_across_branches() {
1392 let mut store = default_store();
1393 let v1 = store.put(b"main-t10".to_vec(), vec![], 10);
1394 store
1395 .create_branch("side".to_owned(), v1, 11)
1396 .expect("create");
1397 store
1398 .put_on_branch("side".to_owned(), b"side-t20".to_vec(), vec![], 20)
1399 .expect("put");
1400 store.put(b"main-t30".to_vec(), vec![], 30);
1401
1402 // At time 15 only v1 (t=10) and the side branch commit (t=20) have
1403 // created_at ≤ 15; v1 is the only one.
1404 let got = store.get(VersionQuery::AtTime(15)).expect("at time");
1405 assert_eq!(got.version, v1);
1406 }
1407
1408 // -----------------------------------------------------------------------
1409 // Test 45 – get(AtTime) when multiple versions share the timestamp
1410 // -----------------------------------------------------------------------
1411 #[test]
1412 fn test_get_at_time_picks_highest_version() {
1413 let mut store = default_store();
1414 let v1 = store.put(b"early".to_vec(), vec![], 100);
1415 let v2 = store.put(b"also-t100".to_vec(), vec![], 100);
1416 let got = store.get(VersionQuery::AtTime(100)).expect("tie");
1417 // Should return v2 (highest version number with created_at ≤ 100).
1418 assert!(got.version == v1 || got.version == v2);
1419 // Specifically the highest:
1420 assert_eq!(got.version, v2);
1421 }
1422
1423 // -----------------------------------------------------------------------
1424 // Test 46 – empty store reachable_from_heads returns empty set
1425 // -----------------------------------------------------------------------
1426 #[test]
1427 fn test_reachable_empty_store() {
1428 let store = default_store();
1429 assert!(store.reachable_from_heads().is_empty());
1430 }
1431
1432 // -----------------------------------------------------------------------
1433 // Test 47 – auto-gc triggered when max_versions is hit
1434 // -----------------------------------------------------------------------
1435 #[test]
1436 fn test_auto_gc_on_max_versions() {
1437 let mut store = ObjectVersionStore::new(VersionStoreConfig {
1438 max_versions: 5,
1439 gc_policy: OvsGcPolicy::KeepTagged,
1440 ..Default::default()
1441 });
1442
1443 // Fill up to max_versions with untagged versions on main.
1444 // All are reachable through the parent chain, so KeepTagged won't
1445 // delete them; but auto-gc is invoked without panic.
1446 for i in 0u8..5 {
1447 store.put(vec![i], vec![], u64::from(i));
1448 }
1449 // Add one more; this triggers GC before insertion.
1450 store.put(vec![99], vec!["important".to_owned()], 99);
1451 // Store is functional and contains at least the tagged version.
1452 assert!(store.get(VersionQuery::Latest).is_ok());
1453 }
1454
1455 // -----------------------------------------------------------------------
1456 // Test 48 – history on branch with single commit
1457 // -----------------------------------------------------------------------
1458 #[test]
1459 fn test_history_single_commit() {
1460 let mut store = default_store();
1461 let v = store.put(b"only".to_vec(), vec![], 1);
1462 let hist = store.history("main").expect("hist");
1463 assert_eq!(hist.len(), 1);
1464 assert_eq!(hist[0].version, v);
1465 }
1466
1467 // -----------------------------------------------------------------------
1468 // Test 49 – put_on_branch advances correct branch, not main
1469 // -----------------------------------------------------------------------
1470 #[test]
1471 fn test_put_on_branch_does_not_advance_main() {
1472 let mut store = default_store();
1473 let v1 = store.put(b"main-base".to_vec(), vec![], 1);
1474 store
1475 .create_branch("side".to_owned(), v1, 2)
1476 .expect("create");
1477 store
1478 .put_on_branch("side".to_owned(), b"side-update".to_vec(), vec![], 3)
1479 .expect("put");
1480
1481 // main head should still be v1.
1482 let main_head = store.branches["main"].head_version;
1483 assert_eq!(main_head, v1);
1484 }
1485
1486 // -----------------------------------------------------------------------
1487 // Test 50 – fnv1a_64 produces correct length hex string
1488 // -----------------------------------------------------------------------
1489 #[test]
1490 fn test_fnv1a_64_hex_length() {
1491 let cid = fnv1a_64(b"test data");
1492 assert_eq!(cid.len(), 16);
1493 assert!(cid.chars().all(|c| c.is_ascii_hexdigit()));
1494 }
1495}