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:
@@ -22,6 +22,6 @@ fn config_serialization_roundtrip() {
|
||||
|
||||
#[test]
|
||||
fn skill_level_toggle() {
|
||||
assert_eq!(SkillLevel::Simple.is_advanced(), false);
|
||||
assert_eq!(SkillLevel::Detailed.is_advanced(), true);
|
||||
assert!(!SkillLevel::Simple.is_advanced());
|
||||
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