Add storage module for presets, config, session, and history persistence
Preset save/load/list/delete/import/export, config JSON persistence, session state save/restore, and processing history log with append/clear. All stored as JSON under ~/.config/pixstrip/.
This commit is contained in:
@@ -19,6 +19,7 @@ webp = "0.3"
|
|||||||
little_exif = "0.4"
|
little_exif = "0.4"
|
||||||
imageproc = "0.25"
|
imageproc = "0.25"
|
||||||
ab_glyph = "0.2"
|
ab_glyph = "0.2"
|
||||||
|
dirs = "6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub mod loader;
|
|||||||
pub mod operations;
|
pub mod operations;
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
pub mod preset;
|
pub mod preset;
|
||||||
|
pub mod storage;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
pub fn version() -> &'static str {
|
pub fn version() -> &'static str {
|
||||||
|
|||||||
287
pixstrip-core/src/storage.rs
Normal file
287
pixstrip-core/src/storage.rs
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
use crate::error::{PixstripError, Result};
|
||||||
|
use crate::preset::Preset;
|
||||||
|
|
||||||
|
fn default_config_dir() -> PathBuf {
|
||||||
|
dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||||
|
.join("pixstrip")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_filename(name: &str) -> String {
|
||||||
|
name.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'/' | '\\' | '<' | '>' | ':' | '"' | '|' | '?' | '*' => '_',
|
||||||
|
_ => c,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Preset Store ---
|
||||||
|
|
||||||
|
pub struct PresetStore {
|
||||||
|
presets_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PresetStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
presets_dir: default_config_dir().join("presets"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_base_dir(base: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
presets_dir: base.join("presets"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_dir(&self) -> Result<()> {
|
||||||
|
std::fs::create_dir_all(&self.presets_dir).map_err(PixstripError::Io)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preset_path(&self, name: &str) -> PathBuf {
|
||||||
|
let filename = format!("{}.json", sanitize_filename(name));
|
||||||
|
self.presets_dir.join(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, preset: &Preset) -> Result<()> {
|
||||||
|
self.ensure_dir()?;
|
||||||
|
let path = self.preset_path(&preset.name);
|
||||||
|
let json = serde_json::to_string_pretty(preset)
|
||||||
|
.map_err(|e| PixstripError::Preset(e.to_string()))?;
|
||||||
|
std::fs::write(&path, json).map_err(PixstripError::Io)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self, name: &str) -> Result<Preset> {
|
||||||
|
let path = self.preset_path(name);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(PixstripError::Preset(format!(
|
||||||
|
"Preset '{}' not found",
|
||||||
|
name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let data = std::fs::read_to_string(&path).map_err(PixstripError::Io)?;
|
||||||
|
serde_json::from_str(&data).map_err(|e| PixstripError::Preset(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> Result<Vec<Preset>> {
|
||||||
|
if !self.presets_dir.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut presets = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(&self.presets_dir).map_err(PixstripError::Io)? {
|
||||||
|
let entry = entry.map_err(PixstripError::Io)?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|e| e.to_str()) == Some("json") {
|
||||||
|
let data = std::fs::read_to_string(&path).map_err(PixstripError::Io)?;
|
||||||
|
if let Ok(preset) = serde_json::from_str::<Preset>(&data) {
|
||||||
|
presets.push(preset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
presets.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(presets)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&self, name: &str) -> Result<()> {
|
||||||
|
let path = self.preset_path(name);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(PixstripError::Preset(format!(
|
||||||
|
"Preset '{}' not found",
|
||||||
|
name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
std::fs::remove_file(&path).map_err(PixstripError::Io)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_to_file(&self, preset: &Preset, path: &Path) -> Result<()> {
|
||||||
|
let json = serde_json::to_string_pretty(preset)
|
||||||
|
.map_err(|e| PixstripError::Preset(e.to_string()))?;
|
||||||
|
std::fs::write(path, json).map_err(PixstripError::Io)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_from_file(&self, path: &Path) -> Result<Preset> {
|
||||||
|
let data = std::fs::read_to_string(path).map_err(PixstripError::Io)?;
|
||||||
|
let mut preset: Preset =
|
||||||
|
serde_json::from_str(&data).map_err(|e| PixstripError::Preset(e.to_string()))?;
|
||||||
|
preset.is_custom = true;
|
||||||
|
Ok(preset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PresetStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Config Store ---
|
||||||
|
|
||||||
|
pub struct ConfigStore {
|
||||||
|
config_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
config_path: default_config_dir().join("config.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_base_dir(base: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
config_path: base.join("config.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, config: &AppConfig) -> Result<()> {
|
||||||
|
if let Some(parent) = self.config_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
|
||||||
|
}
|
||||||
|
let json = serde_json::to_string_pretty(config)
|
||||||
|
.map_err(|e| PixstripError::Config(e.to_string()))?;
|
||||||
|
std::fs::write(&self.config_path, json).map_err(PixstripError::Io)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self) -> Result<AppConfig> {
|
||||||
|
if !self.config_path.exists() {
|
||||||
|
return Ok(AppConfig::default());
|
||||||
|
}
|
||||||
|
let data = std::fs::read_to_string(&self.config_path).map_err(PixstripError::Io)?;
|
||||||
|
serde_json::from_str(&data).map_err(|e| PixstripError::Config(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConfigStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Session Store ---
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct SessionState {
|
||||||
|
pub last_input_dir: Option<String>,
|
||||||
|
pub last_output_dir: Option<String>,
|
||||||
|
pub last_preset_name: Option<String>,
|
||||||
|
pub current_step: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SessionStore {
|
||||||
|
session_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
session_path: default_config_dir().join("session.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_base_dir(base: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
session_path: base.join("session.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, state: &SessionState) -> Result<()> {
|
||||||
|
if let Some(parent) = self.session_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
|
||||||
|
}
|
||||||
|
let json = serde_json::to_string_pretty(state)
|
||||||
|
.map_err(|e| PixstripError::Config(e.to_string()))?;
|
||||||
|
std::fs::write(&self.session_path, json).map_err(PixstripError::Io)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self) -> Result<SessionState> {
|
||||||
|
if !self.session_path.exists() {
|
||||||
|
return Ok(SessionState::default());
|
||||||
|
}
|
||||||
|
let data = std::fs::read_to_string(&self.session_path).map_err(PixstripError::Io)?;
|
||||||
|
serde_json::from_str(&data).map_err(|e| PixstripError::Config(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SessionStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- History Store ---
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct HistoryEntry {
|
||||||
|
pub timestamp: String,
|
||||||
|
pub input_dir: String,
|
||||||
|
pub output_dir: String,
|
||||||
|
pub preset_name: Option<String>,
|
||||||
|
pub total: usize,
|
||||||
|
pub succeeded: usize,
|
||||||
|
pub failed: usize,
|
||||||
|
pub total_input_bytes: u64,
|
||||||
|
pub total_output_bytes: u64,
|
||||||
|
pub elapsed_ms: u64,
|
||||||
|
pub output_files: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HistoryStore {
|
||||||
|
history_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HistoryStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
history_path: default_config_dir().join("history.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_base_dir(base: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
history_path: base.join("history.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&self, entry: HistoryEntry) -> Result<()> {
|
||||||
|
let mut entries = self.list()?;
|
||||||
|
entries.push(entry);
|
||||||
|
self.write_all(&entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> Result<Vec<HistoryEntry>> {
|
||||||
|
if !self.history_path.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let data = std::fs::read_to_string(&self.history_path).map_err(PixstripError::Io)?;
|
||||||
|
if data.trim().is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
serde_json::from_str(&data).map_err(|e| PixstripError::Config(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&self) -> Result<()> {
|
||||||
|
self.write_all(&Vec::<HistoryEntry>::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_all(&self, entries: &[HistoryEntry]) -> Result<()> {
|
||||||
|
if let Some(parent) = self.history_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
|
||||||
|
}
|
||||||
|
let json = serde_json::to_string_pretty(entries)
|
||||||
|
.map_err(|e| PixstripError::Config(e.to_string()))?;
|
||||||
|
std::fs::write(&self.history_path, json).map_err(PixstripError::Io)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HistoryStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,6 @@ fn config_serialization_roundtrip() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn skill_level_toggle() {
|
fn skill_level_toggle() {
|
||||||
assert_eq!(SkillLevel::Simple.is_advanced(), false);
|
assert!(!SkillLevel::Simple.is_advanced());
|
||||||
assert_eq!(SkillLevel::Detailed.is_advanced(), true);
|
assert!(SkillLevel::Detailed.is_advanced());
|
||||||
}
|
}
|
||||||
|
|||||||
278
pixstrip-core/tests/storage_tests.rs
Normal file
278
pixstrip-core/tests/storage_tests.rs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
use pixstrip_core::preset::Preset;
|
||||||
|
use pixstrip_core::storage::*;
|
||||||
|
|
||||||
|
fn with_temp_config_dir(f: impl FnOnce(&std::path::Path)) {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
f(dir.path());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Preset file I/O ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_and_load_preset() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let store = PresetStore::with_base_dir(base);
|
||||||
|
let preset = Preset::builtin_blog_photos();
|
||||||
|
|
||||||
|
store.save(&preset).unwrap();
|
||||||
|
let loaded = store.load("Blog Photos").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded.name, preset.name);
|
||||||
|
assert_eq!(loaded.is_custom, preset.is_custom);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_preset_creates_directory() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let presets_dir = base.join("presets");
|
||||||
|
assert!(!presets_dir.exists());
|
||||||
|
|
||||||
|
let store = PresetStore::with_base_dir(base);
|
||||||
|
let preset = Preset::builtin_blog_photos();
|
||||||
|
store.save(&preset).unwrap();
|
||||||
|
|
||||||
|
assert!(presets_dir.exists());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_saved_presets() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let store = PresetStore::with_base_dir(base);
|
||||||
|
store.save(&Preset::builtin_blog_photos()).unwrap();
|
||||||
|
store.save(&Preset::builtin_privacy_clean()).unwrap();
|
||||||
|
|
||||||
|
let list = store.list().unwrap();
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
|
||||||
|
let names: Vec<&str> = list.iter().map(|p| p.name.as_str()).collect();
|
||||||
|
assert!(names.contains(&"Blog Photos"));
|
||||||
|
assert!(names.contains(&"Privacy Clean"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_preset() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let store = PresetStore::with_base_dir(base);
|
||||||
|
store.save(&Preset::builtin_blog_photos()).unwrap();
|
||||||
|
|
||||||
|
assert!(store.delete("Blog Photos").is_ok());
|
||||||
|
assert!(store.load("Blog Photos").is_err());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_nonexistent_preset_returns_error() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let store = PresetStore::with_base_dir(base);
|
||||||
|
assert!(store.load("Nonexistent").is_err());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preset_filename_sanitization() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let store = PresetStore::with_base_dir(base);
|
||||||
|
let mut preset = Preset::builtin_blog_photos();
|
||||||
|
preset.name = "My Preset / With <Special> Chars!".into();
|
||||||
|
preset.is_custom = true;
|
||||||
|
|
||||||
|
store.save(&preset).unwrap();
|
||||||
|
let loaded = store.load("My Preset / With <Special> Chars!").unwrap();
|
||||||
|
assert_eq!(loaded.name, preset.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Preset import/export ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_and_import_preset() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let store = PresetStore::with_base_dir(base);
|
||||||
|
let preset = Preset::builtin_blog_photos();
|
||||||
|
|
||||||
|
let export_path = base.join("exported.pixstrip-preset");
|
||||||
|
store.export_to_file(&preset, &export_path).unwrap();
|
||||||
|
|
||||||
|
assert!(export_path.exists());
|
||||||
|
|
||||||
|
let imported = store.import_from_file(&export_path).unwrap();
|
||||||
|
assert_eq!(imported.name, preset.name);
|
||||||
|
assert!(imported.is_custom);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_preset_marks_as_custom() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let store = PresetStore::with_base_dir(base);
|
||||||
|
let preset = Preset::builtin_blog_photos();
|
||||||
|
|
||||||
|
let export_path = base.join("test.pixstrip-preset");
|
||||||
|
store.export_to_file(&preset, &export_path).unwrap();
|
||||||
|
|
||||||
|
let imported = store.import_from_file(&export_path).unwrap();
|
||||||
|
assert!(imported.is_custom);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Config persistence ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_and_load_config() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let config_store = ConfigStore::with_base_dir(base);
|
||||||
|
let config = pixstrip_core::config::AppConfig {
|
||||||
|
output_subfolder: "output_images".into(),
|
||||||
|
auto_open_output: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
config_store.save(&config).unwrap();
|
||||||
|
let loaded = config_store.load().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded.output_subfolder, "output_images");
|
||||||
|
assert!(loaded.auto_open_output);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_missing_config_returns_default() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let config_store = ConfigStore::with_base_dir(base);
|
||||||
|
let config = config_store.load().unwrap();
|
||||||
|
let default = pixstrip_core::config::AppConfig::default();
|
||||||
|
|
||||||
|
assert_eq!(config.output_subfolder, default.output_subfolder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Session memory ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn save_and_load_session() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let session_store = SessionStore::with_base_dir(base);
|
||||||
|
let session = SessionState {
|
||||||
|
last_input_dir: Some("/home/user/photos".into()),
|
||||||
|
last_output_dir: Some("/home/user/processed".into()),
|
||||||
|
last_preset_name: Some("Blog Photos".into()),
|
||||||
|
current_step: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
session_store.save(&session).unwrap();
|
||||||
|
let loaded = session_store.load().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded.last_input_dir.as_deref(), Some("/home/user/photos"));
|
||||||
|
assert_eq!(loaded.current_step, 3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_missing_session_returns_default() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let session_store = SessionStore::with_base_dir(base);
|
||||||
|
let session = session_store.load().unwrap();
|
||||||
|
assert_eq!(session.current_step, 0);
|
||||||
|
assert!(session.last_input_dir.is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Processing history ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_and_list_history_entries() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let history = HistoryStore::with_base_dir(base);
|
||||||
|
|
||||||
|
let entry = HistoryEntry {
|
||||||
|
timestamp: "2026-03-06T12:00:00Z".into(),
|
||||||
|
input_dir: "/home/user/photos".into(),
|
||||||
|
output_dir: "/home/user/processed".into(),
|
||||||
|
preset_name: Some("Blog Photos".into()),
|
||||||
|
total: 10,
|
||||||
|
succeeded: 9,
|
||||||
|
failed: 1,
|
||||||
|
total_input_bytes: 50_000_000,
|
||||||
|
total_output_bytes: 10_000_000,
|
||||||
|
elapsed_ms: 5000,
|
||||||
|
output_files: vec![
|
||||||
|
"/home/user/processed/photo1.jpg".into(),
|
||||||
|
"/home/user/processed/photo2.jpg".into(),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
history.add(entry.clone()).unwrap();
|
||||||
|
let entries = history.list().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(entries.len(), 1);
|
||||||
|
assert_eq!(entries[0].total, 10);
|
||||||
|
assert_eq!(entries[0].succeeded, 9);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_appends_entries() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let history = HistoryStore::with_base_dir(base);
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
history
|
||||||
|
.add(HistoryEntry {
|
||||||
|
timestamp: format!("2026-03-06T12:0{}:00Z", i),
|
||||||
|
input_dir: "/input".into(),
|
||||||
|
output_dir: "/output".into(),
|
||||||
|
preset_name: None,
|
||||||
|
total: i + 1,
|
||||||
|
succeeded: i + 1,
|
||||||
|
failed: 0,
|
||||||
|
total_input_bytes: 1000,
|
||||||
|
total_output_bytes: 500,
|
||||||
|
elapsed_ms: 100,
|
||||||
|
output_files: vec![],
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = history.list().unwrap();
|
||||||
|
assert_eq!(entries.len(), 3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_history() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let history = HistoryStore::with_base_dir(base);
|
||||||
|
|
||||||
|
history
|
||||||
|
.add(HistoryEntry {
|
||||||
|
timestamp: "2026-03-06T12:00:00Z".into(),
|
||||||
|
input_dir: "/input".into(),
|
||||||
|
output_dir: "/output".into(),
|
||||||
|
preset_name: None,
|
||||||
|
total: 1,
|
||||||
|
succeeded: 1,
|
||||||
|
failed: 0,
|
||||||
|
total_input_bytes: 1000,
|
||||||
|
total_output_bytes: 500,
|
||||||
|
elapsed_ms: 100,
|
||||||
|
output_files: vec![],
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
history.clear().unwrap();
|
||||||
|
let entries = history.list().unwrap();
|
||||||
|
assert!(entries.is_empty());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_history_returns_empty_list() {
|
||||||
|
with_temp_config_dir(|base| {
|
||||||
|
let history = HistoryStore::with_base_dir(base);
|
||||||
|
let entries = history.list().unwrap();
|
||||||
|
assert!(entries.is_empty());
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user