use std::collections::VecDeque;
use logisheets_base::errors::BasicError;
use crate::controller::status::Status;
const MAX_CHECKPOINTS: usize = 20;
#[derive(Debug, Clone)]
pub struct CheckpointEntry {
pub label: String,
pub description: Option<String>,
pub status: Status,
}
#[derive(Debug)]
pub struct CheckpointManager {
entries: VecDeque<CheckpointEntry>,
}
impl Default for CheckpointManager {
fn default() -> Self {
Self {
entries: VecDeque::new(),
}
}
}
impl CheckpointManager {
pub fn save(
&mut self,
label: String,
description: Option<String>,
status: Status,
) -> &CheckpointEntry {
self.entries.retain(|e| e.label != label);
while self.entries.len() >= MAX_CHECKPOINTS {
self.entries.pop_back();
}
self.entries.push_front(CheckpointEntry {
label,
description,
status,
});
self.entries.front().expect("just pushed")
}
pub fn get(&self, label: &str) -> Result<&Status, BasicError> {
self.entries
.iter()
.find(|e| e.label == label)
.map(|e| &e.status)
.ok_or_else(|| BasicError::CheckpointNotFound(label.to_string()))
}
pub fn delete(&mut self, label: &str) -> bool {
let before = self.entries.len();
self.entries.retain(|e| e.label != label);
self.entries.len() != before
}
pub fn list(&self) -> Vec<CheckpointMeta> {
self.entries
.iter()
.map(|e| CheckpointMeta {
label: e.label.clone(),
description: e.description.clone(),
})
.collect()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
#[derive(Debug, Clone)]
pub struct CheckpointMeta {
pub label: String,
pub description: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_status() -> Status {
Status::default()
}
#[test]
fn save_then_get_returns_snapshot() {
let mut m = CheckpointManager::default();
m.save("a".into(), Some("first".into()), empty_status());
assert!(m.get("a").is_ok());
}
#[test]
fn get_missing_returns_err() {
let m = CheckpointManager::default();
let err = m.get("nope").unwrap_err();
match err {
BasicError::CheckpointNotFound(s) => assert_eq!(s, "nope"),
_ => panic!("expected CheckpointNotFound"),
}
}
#[test]
fn save_with_existing_label_overwrites() {
let mut m = CheckpointManager::default();
m.save("a".into(), Some("v1".into()), empty_status());
m.save("a".into(), Some("v2".into()), empty_status());
assert_eq!(m.len(), 1);
assert_eq!(m.list()[0].description.as_deref(), Some("v2"));
}
#[test]
fn list_is_newest_first() {
let mut m = CheckpointManager::default();
m.save("a".into(), None, empty_status());
m.save("b".into(), None, empty_status());
m.save("c".into(), None, empty_status());
let labels: Vec<_> = m.list().iter().map(|e| e.label.clone()).collect();
assert_eq!(labels, vec!["c", "b", "a"]);
}
#[test]
fn delete_removes_entry() {
let mut m = CheckpointManager::default();
m.save("a".into(), None, empty_status());
assert!(m.delete("a"));
assert!(!m.delete("a")); assert!(m.is_empty());
}
#[test]
fn eviction_drops_oldest_when_full() {
let mut m = CheckpointManager::default();
for i in 0..(MAX_CHECKPOINTS + 5) {
m.save(format!("k{}", i), None, empty_status());
}
assert_eq!(m.len(), MAX_CHECKPOINTS);
assert!(m.get("k0").is_err());
assert!(m.get("k4").is_err());
assert!(m.get("k5").is_ok());
assert!(m.get(&format!("k{}", MAX_CHECKPOINTS + 4)).is_ok());
}
#[test]
fn resave_moves_to_front_so_no_eviction() {
let mut m = CheckpointManager::default();
m.save("keep".into(), None, empty_status());
for i in 0..MAX_CHECKPOINTS {
m.save(format!("k{}", i), None, empty_status());
if i % 2 == 0 {
m.save("keep".into(), None, empty_status());
}
}
assert!(m.get("keep").is_ok());
}
}