diff --git a/cli-bin/build.rs b/cli-bin/build.rs index 22f9fac..08d3ede 100644 --- a/cli-bin/build.rs +++ b/cli-bin/build.rs @@ -28,7 +28,9 @@ fn generate_cheatsheet() -> Result<(), Box> { for (cmd_name_val, cmd_details_val) in cmds { let cmd_name = cmd_name_val.as_str().unwrap_or(""); if let Value::Mapping(cmd_details) = cmd_details_val { - if let Some(Value::Mapping(actions)) = cmd_details.get(&Value::String("actions".into())) { + if let Some(Value::Mapping(actions)) = + cmd_details.get(&Value::String("actions".into())) + { for (action_name_val, action_body_val) in actions { let action_name = action_name_val.as_str().unwrap_or(""); let flags = if let Value::Mapping(action_map) = action_body_val { @@ -45,7 +47,11 @@ fn generate_cheatsheet() -> Result<(), Box> { }; let flags_disp = if flags.is_empty() { "—" } else { &flags }; - table.push_str(&format!("| `{}` | {} |\n", format!("{} {}", cmd_name, action_name), flags_disp)); + table.push_str(&format!( + "| `{}` | {} |\n", + format!("{} {}", cmd_name, action_name), + flags_disp + )); } } } diff --git a/cli-bin/src/cli.rs b/cli-bin/src/cli.rs index 337f6a6..2cc734c 100644 --- a/cli-bin/src/cli.rs +++ b/cli-bin/src/cli.rs @@ -1,14 +1,14 @@ // src/cli.rs -pub mod link; +pub mod annotate; pub mod coll; -pub mod view; +pub mod event; +pub mod link; +pub mod remind; pub mod state; pub mod task; -pub mod remind; -pub mod annotate; pub mod version; -pub mod event; +pub mod view; pub mod watch; use clap::{Parser, Subcommand, ValueEnum}; @@ -77,9 +77,7 @@ pub enum Commands { Backup, /// Restore from a backup file (overwrites current DB) - Restore { - backup_path: std::path::PathBuf, - }, + Restore { backup_path: std::path::PathBuf }, /// Generate shell completions (hidden) #[command(hide = true)] @@ -132,6 +130,12 @@ pub enum Commands { #[derive(Subcommand, Debug)] pub enum AttrCmd { - Set { pattern: String, key: String, value: String }, - Ls { path: std::path::PathBuf }, + Set { + pattern: String, + key: String, + value: String, + }, + Ls { + path: std::path::PathBuf, + }, } diff --git a/cli-bin/src/cli/annotate.rs b/cli-bin/src/cli/annotate.rs index 50db9d5..0a771ef 100644 --- a/cli-bin/src/cli/annotate.rs +++ b/cli-bin/src/cli/annotate.rs @@ -1,11 +1,11 @@ // src/cli/annotate.rs -use clap::{Subcommand, Args}; -use rusqlite::Connection; use crate::cli::Format; +use clap::{Args, Subcommand}; +use rusqlite::Connection; #[derive(Subcommand, Debug)] pub enum AnnotateCmd { - Add (ArgsAdd), + Add(ArgsAdd), List(ArgsList), } @@ -13,16 +13,20 @@ pub enum AnnotateCmd { pub struct ArgsAdd { pub file: String, pub note: String, - #[arg(long)] pub range: Option, - #[arg(long)] pub highlight: bool, + #[arg(long)] + pub range: Option, + #[arg(long)] + pub highlight: bool, } #[derive(Args, Debug)] -pub struct ArgsList { pub file_pattern: String } +pub struct ArgsList { + pub file_pattern: String, +} pub fn run(cmd: &AnnotateCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> { match cmd { - AnnotateCmd::Add(a) => todo!("annotate add {:?}", a), + AnnotateCmd::Add(a) => todo!("annotate add {:?}", a), AnnotateCmd::List(a) => todo!("annotate list {:?}", a), } } diff --git a/cli-bin/src/cli/coll.rs b/cli-bin/src/cli/coll.rs index 91a7545..c5521e2 100644 --- a/cli-bin/src/cli/coll.rs +++ b/cli-bin/src/cli/coll.rs @@ -3,8 +3,8 @@ use clap::{Args, Subcommand}; use rusqlite::Connection; -use crate::cli::Format; // local enum for text / json output -use libmarlin::db; // core DB helpers from the library crate +use crate::cli::Format; // local enum for text / json output +use libmarlin::db; // core DB helpers from the library crate #[derive(Subcommand, Debug)] pub enum CollCmd { @@ -36,11 +36,9 @@ pub struct ListArgs { /// /// Returns the collection ID or an error if it doesn’t exist. fn lookup_collection_id(conn: &Connection, name: &str) -> anyhow::Result { - conn.query_row( - "SELECT id FROM collections WHERE name = ?1", - [name], - |r| r.get(0), - ) + conn.query_row("SELECT id FROM collections WHERE name = ?1", [name], |r| { + r.get(0) + }) .map_err(|_| anyhow::anyhow!("collection not found: {}", name)) } @@ -74,11 +72,7 @@ pub fn run(cmd: &CollCmd, conn: &mut Connection, fmt: Format) -> anyhow::Result< Format::Json => { #[cfg(feature = "json")] { - println!( - "{{\"collection\":\"{}\",\"added\":{}}}", - a.name, - ids.len() - ); + println!("{{\"collection\":\"{}\",\"added\":{}}}", a.name, ids.len()); } } } diff --git a/cli-bin/src/cli/event.rs b/cli-bin/src/cli/event.rs index 6988be6..ed36c9e 100644 --- a/cli-bin/src/cli/event.rs +++ b/cli-bin/src/cli/event.rs @@ -1,11 +1,11 @@ // src/cli/event.rs -use clap::{Subcommand, Args}; -use rusqlite::Connection; use crate::cli::Format; +use clap::{Args, Subcommand}; +use rusqlite::Connection; #[derive(Subcommand, Debug)] pub enum EventCmd { - Add (ArgsAdd), + Add(ArgsAdd), Timeline, } @@ -18,7 +18,7 @@ pub struct ArgsAdd { pub fn run(cmd: &EventCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> { match cmd { - EventCmd::Add(a) => todo!("event add {:?}", a), - EventCmd::Timeline => todo!("event timeline"), + EventCmd::Add(a) => todo!("event add {:?}", a), + EventCmd::Timeline => todo!("event timeline"), } } diff --git a/cli-bin/src/cli/link.rs b/cli-bin/src/cli/link.rs index 70f538b..be6830a 100644 --- a/cli-bin/src/cli/link.rs +++ b/cli-bin/src/cli/link.rs @@ -1,15 +1,15 @@ //! src/cli/link.rs – manage typed relationships between files -use clap::{Subcommand, Args}; +use clap::{Args, Subcommand}; use rusqlite::Connection; -use crate::cli::Format; // output selector -use libmarlin::db; // ← switched from `crate::db` +use crate::cli::Format; // output selector +use libmarlin::db; // ← switched from `crate::db` #[derive(Subcommand, Debug)] pub enum LinkCmd { Add(LinkArgs), - Rm (LinkArgs), + Rm(LinkArgs), List(ListArgs), Backlinks(BacklinksArgs), } @@ -17,7 +17,7 @@ pub enum LinkCmd { #[derive(Args, Debug)] pub struct LinkArgs { pub from: String, - pub to: String, + pub to: String, #[arg(long)] pub r#type: Option, } @@ -70,7 +70,10 @@ pub fn run(cmd: &LinkCmd, conn: &mut Connection, format: Format) -> anyhow::Resu match format { Format::Text => { if let Some(t) = &args.r#type { - println!("Removed link '{}' → '{}' [type='{}']", args.from, args.to, t); + println!( + "Removed link '{}' → '{}' [type='{}']", + args.from, args.to, t + ); } else { println!("Removed link '{}' → '{}'", args.from, args.to); } diff --git a/cli-bin/src/cli/remind.rs b/cli-bin/src/cli/remind.rs index 99dac34..2f65a3f 100644 --- a/cli-bin/src/cli/remind.rs +++ b/cli-bin/src/cli/remind.rs @@ -1,7 +1,7 @@ // src/cli/remind.rs -use clap::{Subcommand, Args}; -use rusqlite::Connection; use crate::cli::Format; +use clap::{Args, Subcommand}; +use rusqlite::Connection; #[derive(Subcommand, Debug)] pub enum RemindCmd { @@ -11,8 +11,8 @@ pub enum RemindCmd { #[derive(Args, Debug)] pub struct ArgsSet { pub file_pattern: String, - pub timestamp: String, - pub message: String, + pub timestamp: String, + pub message: String, } pub fn run(cmd: &RemindCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> { diff --git a/cli-bin/src/cli/state.rs b/cli-bin/src/cli/state.rs index 7ac3628..e72782b 100644 --- a/cli-bin/src/cli/state.rs +++ b/cli-bin/src/cli/state.rs @@ -1,7 +1,7 @@ // src/cli/state.rs -use clap::{Subcommand, Args}; -use rusqlite::Connection; use crate::cli::Format; +use clap::{Args, Subcommand}; +use rusqlite::Connection; #[derive(Subcommand, Debug)] pub enum StateCmd { @@ -11,16 +11,24 @@ pub enum StateCmd { } #[derive(Args, Debug)] -pub struct ArgsSet { pub file_pattern: String, pub new_state: String } +pub struct ArgsSet { + pub file_pattern: String, + pub new_state: String, +} #[derive(Args, Debug)] -pub struct ArgsTrans { pub from_state: String, pub to_state: String } +pub struct ArgsTrans { + pub from_state: String, + pub to_state: String, +} #[derive(Args, Debug)] -pub struct ArgsLog { pub file_pattern: String } +pub struct ArgsLog { + pub file_pattern: String, +} pub fn run(cmd: &StateCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> { match cmd { - StateCmd::Set(a) => todo!("state set {:?}", a), - StateCmd::TransitionsAdd(a)=> todo!("state transitions-add {:?}", a), - StateCmd::Log(a) => todo!("state log {:?}", a), + StateCmd::Set(a) => todo!("state set {:?}", a), + StateCmd::TransitionsAdd(a) => todo!("state transitions-add {:?}", a), + StateCmd::Log(a) => todo!("state log {:?}", a), } } diff --git a/cli-bin/src/cli/task.rs b/cli-bin/src/cli/task.rs index 57f9d4c..cf0f1a0 100644 --- a/cli-bin/src/cli/task.rs +++ b/cli-bin/src/cli/task.rs @@ -1,7 +1,7 @@ // src/cli/task.rs -use clap::{Subcommand, Args}; -use rusqlite::Connection; use crate::cli::Format; +use clap::{Args, Subcommand}; +use rusqlite::Connection; #[derive(Subcommand, Debug)] pub enum TaskCmd { @@ -10,9 +10,14 @@ pub enum TaskCmd { } #[derive(Args, Debug)] -pub struct ArgsScan { pub directory: String } +pub struct ArgsScan { + pub directory: String, +} #[derive(Args, Debug)] -pub struct ArgsList { #[arg(long)] pub due_today: bool } +pub struct ArgsList { + #[arg(long)] + pub due_today: bool, +} pub fn run(cmd: &TaskCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> { match cmd { diff --git a/cli-bin/src/cli/version.rs b/cli-bin/src/cli/version.rs index 0c5bf26..de2ee58 100644 --- a/cli-bin/src/cli/version.rs +++ b/cli-bin/src/cli/version.rs @@ -1,7 +1,7 @@ // src/cli/version.rs -use clap::{Subcommand, Args}; -use rusqlite::Connection; use crate::cli::Format; +use clap::{Args, Subcommand}; +use rusqlite::Connection; #[derive(Subcommand, Debug)] pub enum VersionCmd { @@ -9,7 +9,9 @@ pub enum VersionCmd { } #[derive(Args, Debug)] -pub struct ArgsDiff { pub file: String } +pub struct ArgsDiff { + pub file: String, +} pub fn run(cmd: &VersionCmd, _conn: &mut Connection, _format: Format) -> anyhow::Result<()> { match cmd { diff --git a/cli-bin/src/cli/view.rs b/cli-bin/src/cli/view.rs index 46cf2c3..5ce9952 100644 --- a/cli-bin/src/cli/view.rs +++ b/cli-bin/src/cli/view.rs @@ -6,8 +6,8 @@ use anyhow::Result; use clap::{Args, Subcommand}; use rusqlite::Connection; -use crate::cli::Format; // output selector stays local -use libmarlin::db; // ← path switched from `crate::db` +use crate::cli::Format; // output selector stays local +use libmarlin::db; // ← path switched from `crate::db` #[derive(Subcommand, Debug)] pub enum ViewCmd { diff --git a/cli-bin/src/cli/watch.rs b/cli-bin/src/cli/watch.rs index 338f6b5..24b9a60 100644 --- a/cli-bin/src/cli/watch.rs +++ b/cli-bin/src/cli/watch.rs @@ -5,8 +5,8 @@ use clap::Subcommand; use libmarlin::watcher::{WatcherConfig, WatcherState}; use rusqlite::Connection; use std::path::PathBuf; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::thread; use std::time::{Duration, Instant}; use tracing::info; @@ -30,15 +30,15 @@ pub enum WatchCmd { /// Directory to watch (defaults to current directory) #[arg(default_value = ".")] path: PathBuf, - + /// Debounce window in milliseconds (default: 100ms) #[arg(long, default_value = "100")] debounce_ms: u64, }, - + /// Show status of currently active watcher Status, - + /// Stop the currently running watcher Stop, } @@ -60,7 +60,7 @@ pub fn run(cmd: &WatchCmd, _conn: &mut Connection, _format: super::Format) -> Re let status = watcher.status()?; info!("Watcher started. Press Ctrl+C to stop watching."); info!("Watching {} paths", status.watched_paths.len()); - + let start_time = Instant::now(); let mut last_status_time = Instant::now(); let running = Arc::new(AtomicBool::new(true)); @@ -80,7 +80,7 @@ pub fn run(cmd: &WatchCmd, _conn: &mut Connection, _format: super::Format) -> Re } // Corrected line: removed the extra closing parenthesis - if last_status_time.elapsed() > Duration::from_secs(10) { + if last_status_time.elapsed() > Duration::from_secs(10) { let uptime = start_time.elapsed(); info!( "Watcher running for {}s, processed {} events, queue: {}, state: {:?}", @@ -104,7 +104,9 @@ pub fn run(cmd: &WatchCmd, _conn: &mut Connection, _format: super::Format) -> Re Ok(()) } WatchCmd::Status => { - info!("Status command: No active watcher process to query in this CLI invocation model."); + info!( + "Status command: No active watcher process to query in this CLI invocation model." + ); info!("To see live status, run 'marlin watch start' which prints periodic updates."); Ok(()) } diff --git a/cli-bin/src/main.rs b/cli-bin/src/main.rs index 5df976a..5538dff 100644 --- a/cli-bin/src/main.rs +++ b/cli-bin/src/main.rs @@ -9,14 +9,8 @@ mod cli; // sub-command definitions and argument structs /* ── shared modules re-exported from libmarlin ─────────────────── */ -use libmarlin::{ - config, - db, - logging, - scan, - utils::determine_scan_root, -}; use libmarlin::db::take_dirty; +use libmarlin::{config, db, logging, scan, utils::determine_scan_root}; use anyhow::{Context, Result}; use clap::{CommandFactory, Parser}; @@ -24,13 +18,7 @@ use clap_complete::generate; use glob::Pattern; use shellexpand; use shlex; -use std::{ - env, - fs, - io, - path::Path, - process::Command, -}; +use std::{env, fs, io, path::Path, process::Command}; use tracing::{debug, error, info}; use walkdir::WalkDir; @@ -57,7 +45,7 @@ fn main() -> Result<()> { match &args.command { Commands::Init | Commands::Backup | Commands::Restore { .. } => {} _ => match db::backup(&cfg.db_path) { - Ok(p) => info!("Pre-command auto-backup created at {}", p.display()), + Ok(p) => info!("Pre-command auto-backup created at {}", p.display()), Err(e) => error!("Failed to create pre-command auto-backup: {e}"), }, } @@ -72,9 +60,8 @@ fn main() -> Result<()> { /* ---- init ------------------------------------------------ */ Commands::Init => { info!("Database initialised at {}", cfg.db_path.display()); - let cwd = env::current_dir().context("getting current directory")?; - let count = scan::scan_directory(&mut conn, &cwd) - .context("initial scan failed")?; + let cwd = env::current_dir().context("getting current directory")?; + let count = scan::scan_directory(&mut conn, &cwd).context("initial scan failed")?; info!("Initial scan complete – indexed/updated {count} files"); } @@ -89,11 +76,8 @@ fn main() -> Result<()> { if dirty { let dirty_ids = take_dirty(&conn)?; for id in dirty_ids { - let path: String = conn.query_row( - "SELECT path FROM files WHERE id = ?1", - [id], - |r| r.get(0), - )?; + let path: String = + conn.query_row("SELECT path FROM files WHERE id = ?1", [id], |r| r.get(0))?; scan::scan_directory(&mut conn, Path::new(&path))?; } } else { @@ -104,18 +88,18 @@ fn main() -> Result<()> { } /* ---- tag / attribute / search --------------------------- */ - Commands::Tag { pattern, tag_path } => - apply_tag(&conn, &pattern, &tag_path)?, + Commands::Tag { pattern, tag_path } => apply_tag(&conn, &pattern, &tag_path)?, Commands::Attr { action } => match action { - cli::AttrCmd::Set { pattern, key, value } => - attr_set(&conn, &pattern, &key, &value)?, - cli::AttrCmd::Ls { path } => - attr_ls(&conn, &path)?, + cli::AttrCmd::Set { + pattern, + key, + value, + } => attr_set(&conn, &pattern, &key, &value)?, + cli::AttrCmd::Ls { path } => attr_ls(&conn, &path)?, }, - Commands::Search { query, exec } => - run_search(&conn, &query, exec)?, + Commands::Search { query, exec } => run_search(&conn, &query, exec)?, /* ---- maintenance ---------------------------------------- */ Commands::Backup => { @@ -125,9 +109,8 @@ fn main() -> Result<()> { Commands::Restore { backup_path } => { drop(conn); - db::restore(&backup_path, &cfg.db_path).with_context(|| { - format!("Failed to restore DB from {}", backup_path.display()) - })?; + db::restore(&backup_path, &cfg.db_path) + .with_context(|| format!("Failed to restore DB from {}", backup_path.display()))?; println!("Restored DB from {}", backup_path.display()); db::open(&cfg.db_path).with_context(|| { format!("Could not open restored DB at {}", cfg.db_path.display()) @@ -136,15 +119,15 @@ fn main() -> Result<()> { } /* ---- passthrough sub-modules (some still stubs) ---------- */ - Commands::Link(link_cmd) => cli::link::run(&link_cmd, &mut conn, args.format)?, - Commands::Coll(coll_cmd) => cli::coll::run(&coll_cmd, &mut conn, args.format)?, - Commands::View(view_cmd) => cli::view::run(&view_cmd, &mut conn, args.format)?, + Commands::Link(link_cmd) => cli::link::run(&link_cmd, &mut conn, args.format)?, + Commands::Coll(coll_cmd) => cli::coll::run(&coll_cmd, &mut conn, args.format)?, + Commands::View(view_cmd) => cli::view::run(&view_cmd, &mut conn, args.format)?, Commands::State(state_cmd) => cli::state::run(&state_cmd, &mut conn, args.format)?, - Commands::Task(task_cmd) => cli::task::run(&task_cmd, &mut conn, args.format)?, - Commands::Remind(rm_cmd) => cli::remind::run(&rm_cmd, &mut conn, args.format)?, - Commands::Annotate(a_cmd) => cli::annotate::run(&a_cmd, &mut conn, args.format)?, - Commands::Version(v_cmd) => cli::version::run(&v_cmd, &mut conn, args.format)?, - Commands::Event(e_cmd) => cli::event::run(&e_cmd, &mut conn, args.format)?, + Commands::Task(task_cmd) => cli::task::run(&task_cmd, &mut conn, args.format)?, + Commands::Remind(rm_cmd) => cli::remind::run(&rm_cmd, &mut conn, args.format)?, + Commands::Annotate(a_cmd) => cli::annotate::run(&a_cmd, &mut conn, args.format)?, + Commands::Version(v_cmd) => cli::version::run(&v_cmd, &mut conn, args.format)?, + Commands::Event(e_cmd) => cli::event::run(&e_cmd, &mut conn, args.format)?, Commands::Watch(watch_cmd) => cli::watch::run(&watch_cmd, &mut conn, args.format)?, } @@ -160,22 +143,19 @@ fn apply_tag(conn: &rusqlite::Connection, pattern: &str, tag_path: &str) -> Resu let mut current = Some(leaf_tag_id); while let Some(id) = current { tag_ids.push(id); - current = conn.query_row( - "SELECT parent_id FROM tags WHERE id=?1", - [id], - |r| r.get::<_, Option>(0), - )?; + current = conn.query_row("SELECT parent_id FROM tags WHERE id=?1", [id], |r| { + r.get::<_, Option>(0) + })?; } let expanded = shellexpand::tilde(pattern).into_owned(); - let pat = Pattern::new(&expanded) - .with_context(|| format!("Invalid glob pattern `{expanded}`"))?; + let pat = + Pattern::new(&expanded).with_context(|| format!("Invalid glob pattern `{expanded}`"))?; let root = determine_scan_root(&expanded); - let mut stmt_file = conn.prepare("SELECT id FROM files WHERE path=?1")?; - let mut stmt_insert = conn.prepare( - "INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)", - )?; + let mut stmt_file = conn.prepare("SELECT id FROM files WHERE path=?1")?; + let mut stmt_insert = + conn.prepare("INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)")?; let mut count = 0usize; for entry in WalkDir::new(&root) @@ -184,7 +164,9 @@ fn apply_tag(conn: &rusqlite::Connection, pattern: &str, tag_path: &str) -> Resu .filter(|e| e.file_type().is_file()) { let p = entry.path().to_string_lossy(); - if !pat.matches(&p) { continue; } + if !pat.matches(&p) { + continue; + } match stmt_file.query_row([p.as_ref()], |r| r.get::<_, i64>(0)) { Ok(fid) => { @@ -199,10 +181,10 @@ fn apply_tag(conn: &rusqlite::Connection, pattern: &str, tag_path: &str) -> Resu count += 1; } } - Err(rusqlite::Error::QueryReturnedNoRows) => - error!(file=%p, "not indexed – run `marlin scan` first"), - Err(e) => - error!(file=%p, error=%e, "could not lookup file ID"), + Err(rusqlite::Error::QueryReturnedNoRows) => { + error!(file=%p, "not indexed – run `marlin scan` first") + } + Err(e) => error!(file=%p, error=%e, "could not lookup file ID"), } } @@ -213,8 +195,8 @@ fn apply_tag(conn: &rusqlite::Connection, pattern: &str, tag_path: &str) -> Resu /* ---------- ATTRIBUTES ---------- */ fn attr_set(conn: &rusqlite::Connection, pattern: &str, key: &str, value: &str) -> Result<()> { let expanded = shellexpand::tilde(pattern).into_owned(); - let pat = Pattern::new(&expanded) - .with_context(|| format!("Invalid glob pattern `{expanded}`"))?; + let pat = + Pattern::new(&expanded).with_context(|| format!("Invalid glob pattern `{expanded}`"))?; let root = determine_scan_root(&expanded); let mut stmt_file = conn.prepare("SELECT id FROM files WHERE path=?1")?; @@ -226,7 +208,9 @@ fn attr_set(conn: &rusqlite::Connection, pattern: &str, key: &str, value: &str) .filter(|e| e.file_type().is_file()) { let p = entry.path().to_string_lossy(); - if !pat.matches(&p) { continue; } + if !pat.matches(&p) { + continue; + } match stmt_file.query_row([p.as_ref()], |r| r.get::<_, i64>(0)) { Ok(fid) => { @@ -234,10 +218,10 @@ fn attr_set(conn: &rusqlite::Connection, pattern: &str, key: &str, value: &str) info!(file=%p, key, value, "attr set"); count += 1; } - Err(rusqlite::Error::QueryReturnedNoRows) => - error!(file=%p, "not indexed – run `marlin scan` first"), - Err(e) => - error!(file=%p, error=%e, "could not lookup file ID"), + Err(rusqlite::Error::QueryReturnedNoRows) => { + error!(file=%p, "not indexed – run `marlin scan` first") + } + Err(e) => error!(file=%p, error=%e, "could not lookup file ID"), } } @@ -247,12 +231,11 @@ fn attr_set(conn: &rusqlite::Connection, pattern: &str, key: &str, value: &str) fn attr_ls(conn: &rusqlite::Connection, path: &Path) -> Result<()> { let fid = db::file_id(conn, &path.to_string_lossy())?; - let mut stmt = conn.prepare( - "SELECT key, value FROM attributes WHERE file_id=?1 ORDER BY key" - )?; - for row in stmt - .query_map([fid], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))? - { + let mut stmt = + conn.prepare("SELECT key, value FROM attributes WHERE file_id=?1 ORDER BY key")?; + for row in stmt.query_map([fid], |r| { + Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)) + })? { let (k, v) = row?; println!("{k} = {v}"); } @@ -268,7 +251,9 @@ fn run_search(conn: &rusqlite::Connection, raw_query: &str, exec: Option parts.push(tok); } else if let Some(tag) = tok.strip_prefix("tag:") { for (i, seg) in tag.split('/').filter(|s| !s.is_empty()).enumerate() { - if i > 0 { parts.push("AND".into()); } + if i > 0 { + parts.push("AND".into()); + } parts.push(format!("tags_text:{}", escape_fts(seg))); } } else if let Some(attr) = tok.strip_prefix("attr:") { @@ -310,11 +295,11 @@ fn run_search(conn: &rusqlite::Connection, raw_query: &str, exec: Option run_exec(&hits, &cmd_tpl)?; } else { if hits.is_empty() { - eprintln!( - "No matches for query: `{raw_query}` (FTS expr: `{fts_expr}`)" - ); + eprintln!("No matches for query: `{raw_query}` (FTS expr: `{fts_expr}`)"); } else { - for p in hits { println!("{p}"); } + for p in hits { + println!("{p}"); + } } } Ok(()) @@ -333,7 +318,9 @@ fn naive_substring_search(conn: &rusqlite::Connection, term: &str) -> Result 65_536 { continue; } + if meta.len() > 65_536 { + continue; + } } if let Ok(body) = fs::read_to_string(&p) { if body.to_lowercase().contains(&needle) { @@ -369,7 +356,9 @@ fn run_exec(paths: &[String], cmd_tpl: &str) -> Result<()> { format!("{cmd_tpl} {quoted}") }; if let Some(mut parts) = shlex::split(&final_cmd) { - if parts.is_empty() { continue; } + if parts.is_empty() { + continue; + } let prog = parts.remove(0); let status = Command::new(&prog).args(parts).status()?; if !status.success() { @@ -393,9 +382,9 @@ fn escape_fts(term: &str) -> String { #[cfg(test)] mod tests { + use super::{apply_tag, attr_set, escape_fts, naive_substring_search, run_exec}; use assert_cmd::Command; use tempfile::tempdir; - use super::{apply_tag, attr_set, naive_substring_search, run_exec, escape_fts}; #[test] fn test_help_command() { @@ -483,7 +472,10 @@ mod tests { cmd_scan.env("MARLIN_DB_PATH", &db_path); cmd_scan.arg("scan"); cmd_scan.assert().success(); - assert!(backups_dir.exists(), "Backups directory should exist after scan"); + assert!( + backups_dir.exists(), + "Backups directory should exist after scan" + ); let backups: Vec<_> = backups_dir.read_dir().unwrap().collect(); assert_eq!(backups.len(), 1, "One backup should be created for scan"); } @@ -504,7 +496,11 @@ mod tests { let tmp = tempdir().unwrap(); let mut cmd = Command::cargo_bin("marlin").unwrap(); cmd.env("MARLIN_DB_PATH", tmp.path().join("index.db")); - cmd.arg("event").arg("add").arg("file.txt").arg("2025-05-20").arg("desc"); + cmd.arg("event") + .arg("add") + .arg("file.txt") + .arg("2025-05-20") + .arg("desc"); cmd.assert() .failure() .stderr(predicates::str::contains("not yet implemented")); @@ -516,8 +512,8 @@ mod tests { #[test] fn test_tagging_and_attributes_update_db() { - use std::fs::File; use libmarlin::scan::scan_directory; + use std::fs::File; let tmp = tempdir().unwrap(); let file_path = tmp.path().join("a.txt"); @@ -567,7 +563,11 @@ mod tests { fs::write(&script, "#!/bin/sh\necho $1 >> $LOGFILE\n").unwrap(); std::env::set_var("LOGFILE", &log); - run_exec(&[f1.to_string_lossy().to_string()], &format!("sh {} {{}}", script.display())).unwrap(); + run_exec( + &[f1.to_string_lossy().to_string()], + &format!("sh {} {{}}", script.display()), + ) + .unwrap(); let logged = fs::read_to_string(&log).unwrap(); assert!(logged.contains("hello.txt")); } diff --git a/cli-bin/tests/cli_coll_unit.rs b/cli-bin/tests/cli_coll_unit.rs index e287680..21b3ba4 100644 --- a/cli-bin/tests/cli_coll_unit.rs +++ b/cli-bin/tests/cli_coll_unit.rs @@ -1,6 +1,9 @@ mod cli { #[derive(Clone, Copy, Debug)] - pub enum Format { Text, Json } + pub enum Format { + Text, + Json, + } } #[path = "../src/cli/coll.rs"] @@ -11,20 +14,41 @@ use libmarlin::db; #[test] fn coll_run_creates_and_adds() { let mut conn = db::open(":memory:").unwrap(); - conn.execute("INSERT INTO files(path,size,mtime) VALUES ('a.txt',0,0)", []).unwrap(); - conn.execute("INSERT INTO files(path,size,mtime) VALUES ('b.txt',0,0)", []).unwrap(); + conn.execute( + "INSERT INTO files(path,size,mtime) VALUES ('a.txt',0,0)", + [], + ) + .unwrap(); + conn.execute( + "INSERT INTO files(path,size,mtime) VALUES ('b.txt',0,0)", + [], + ) + .unwrap(); - let create = coll::CollCmd::Create(coll::CreateArgs{ name: "Set".into() }); + let create = coll::CollCmd::Create(coll::CreateArgs { name: "Set".into() }); coll::run(&create, &mut conn, cli::Format::Text).unwrap(); - let coll_id: i64 = conn.query_row("SELECT id FROM collections WHERE name='Set'", [], |r| r.get(0)).unwrap(); + let coll_id: i64 = conn + .query_row("SELECT id FROM collections WHERE name='Set'", [], |r| { + r.get(0) + }) + .unwrap(); - let add = coll::CollCmd::Add(coll::AddArgs{ name: "Set".into(), file_pattern: "*.txt".into() }); + let add = coll::CollCmd::Add(coll::AddArgs { + name: "Set".into(), + file_pattern: "*.txt".into(), + }); coll::run(&add, &mut conn, cli::Format::Text).unwrap(); - let cnt: i64 = conn.query_row("SELECT COUNT(*) FROM collection_files WHERE collection_id=?1", [coll_id], |r| r.get(0)).unwrap(); + let cnt: i64 = conn + .query_row( + "SELECT COUNT(*) FROM collection_files WHERE collection_id=?1", + [coll_id], + |r| r.get(0), + ) + .unwrap(); assert_eq!(cnt, 2); - let list = coll::CollCmd::List(coll::ListArgs{ name: "Set".into() }); + let list = coll::CollCmd::List(coll::ListArgs { name: "Set".into() }); coll::run(&list, &mut conn, cli::Format::Text).unwrap(); } diff --git a/cli-bin/tests/cli_link_unit.rs b/cli-bin/tests/cli_link_unit.rs index 5922bcd..b34d5da 100644 --- a/cli-bin/tests/cli_link_unit.rs +++ b/cli-bin/tests/cli_link_unit.rs @@ -1,6 +1,9 @@ mod cli { #[derive(Clone, Copy, Debug)] - pub enum Format { Text, Json } + pub enum Format { + Text, + Json, + } } #[path = "../src/cli/link.rs"] @@ -11,27 +14,43 @@ use libmarlin::db; #[test] fn link_run_add_and_rm() { let mut conn = db::open(":memory:").unwrap(); - conn.execute("INSERT INTO files(path,size,mtime) VALUES ('foo.txt',0,0)", []).unwrap(); - conn.execute("INSERT INTO files(path,size,mtime) VALUES ('bar.txt',0,0)", []).unwrap(); + conn.execute( + "INSERT INTO files(path,size,mtime) VALUES ('foo.txt',0,0)", + [], + ) + .unwrap(); + conn.execute( + "INSERT INTO files(path,size,mtime) VALUES ('bar.txt',0,0)", + [], + ) + .unwrap(); let add = link::LinkCmd::Add(link::LinkArgs { from: "foo.txt".into(), - to: "bar.txt".into(), + to: "bar.txt".into(), r#type: None, }); link::run(&add, &mut conn, cli::Format::Text).unwrap(); - let count: i64 = conn.query_row("SELECT COUNT(*) FROM links", [], |r| r.get(0)).unwrap(); + let count: i64 = conn + .query_row("SELECT COUNT(*) FROM links", [], |r| r.get(0)) + .unwrap(); assert_eq!(count, 1); - let list = link::LinkCmd::List(link::ListArgs { pattern: "foo.txt".into(), direction: None, r#type: None }); + let list = link::LinkCmd::List(link::ListArgs { + pattern: "foo.txt".into(), + direction: None, + r#type: None, + }); link::run(&list, &mut conn, cli::Format::Text).unwrap(); let rm = link::LinkCmd::Rm(link::LinkArgs { from: "foo.txt".into(), - to: "bar.txt".into(), + to: "bar.txt".into(), r#type: None, }); link::run(&rm, &mut conn, cli::Format::Text).unwrap(); - let remaining: i64 = conn.query_row("SELECT COUNT(*) FROM links", [], |r| r.get(0)).unwrap(); + let remaining: i64 = conn + .query_row("SELECT COUNT(*) FROM links", [], |r| r.get(0)) + .unwrap(); assert_eq!(remaining, 0); } diff --git a/cli-bin/tests/cli_view_unit.rs b/cli-bin/tests/cli_view_unit.rs index c2f0338..7a507a8 100644 --- a/cli-bin/tests/cli_view_unit.rs +++ b/cli-bin/tests/cli_view_unit.rs @@ -1,6 +1,9 @@ mod cli { #[derive(Clone, Copy, Debug)] - pub enum Format { Text, Json } + pub enum Format { + Text, + Json, + } } #[path = "../src/cli/view.rs"] @@ -11,17 +14,30 @@ use libmarlin::db; #[test] fn view_run_save_and_exec() { let mut conn = db::open(":memory:").unwrap(); - conn.execute("INSERT INTO files(path,size,mtime) VALUES ('TODO.txt',0,0)", []).unwrap(); + conn.execute( + "INSERT INTO files(path,size,mtime) VALUES ('TODO.txt',0,0)", + [], + ) + .unwrap(); - let save = view::ViewCmd::Save(view::ArgsSave { view_name: "tasks".into(), query: "TODO".into() }); + let save = view::ViewCmd::Save(view::ArgsSave { + view_name: "tasks".into(), + query: "TODO".into(), + }); view::run(&save, &mut conn, cli::Format::Text).unwrap(); - let stored: String = conn.query_row("SELECT query FROM views WHERE name='tasks'", [], |r| r.get(0)).unwrap(); + let stored: String = conn + .query_row("SELECT query FROM views WHERE name='tasks'", [], |r| { + r.get(0) + }) + .unwrap(); assert_eq!(stored, "TODO"); let list = view::ViewCmd::List; view::run(&list, &mut conn, cli::Format::Text).unwrap(); - let exec = view::ViewCmd::Exec(view::ArgsExec { view_name: "tasks".into() }); + let exec = view::ViewCmd::Exec(view::ArgsExec { + view_name: "tasks".into(), + }); view::run(&exec, &mut conn, cli::Format::Text).unwrap(); } diff --git a/cli-bin/tests/e2e.rs b/cli-bin/tests/e2e.rs index e946dce..2165cb2 100644 --- a/cli-bin/tests/e2e.rs +++ b/cli-bin/tests/e2e.rs @@ -25,8 +25,8 @@ fn spawn_demo_tree(root: &PathBuf) { fs::write(root.join("Projects/Alpha/draft2.md"), "- [x] TODO foo\n").unwrap(); fs::write(root.join("Projects/Beta/final.md"), "done\n").unwrap(); fs::write(root.join("Projects/Gamma/TODO.txt"), "TODO bar\n").unwrap(); - fs::write(root.join("Logs/app.log"), "ERROR omg\n").unwrap(); - fs::write(root.join("Reports/Q1.pdf"), "PDF\n").unwrap(); + fs::write(root.join("Logs/app.log"), "ERROR omg\n").unwrap(); + fs::write(root.join("Reports/Q1.pdf"), "PDF\n").unwrap(); } /// Shorthand for “run and must succeed”. @@ -38,7 +38,7 @@ fn ok(cmd: &mut Command) -> assert_cmd::assert::Assert { fn full_cli_flow() -> Result<(), Box> { /* ── 1 ░ sandbox ───────────────────────────────────────────── */ - let tmp = tempdir()?; // wiped on drop + let tmp = tempdir()?; // wiped on drop let demo_dir = tmp.path().join("marlin_demo"); spawn_demo_tree(&demo_dir); @@ -53,9 +53,7 @@ fn full_cli_flow() -> Result<(), Box> { /* ── 2 ░ init ( auto-scan cwd ) ───────────────────────────── */ - ok(marlin() - .current_dir(&demo_dir) - .arg("init")); + ok(marlin().current_dir(&demo_dir).arg("init")); /* ── 3 ░ tag & attr demos ─────────────────────────────────── */ @@ -74,12 +72,14 @@ fn full_cli_flow() -> Result<(), Box> { /* ── 4 ░ quick search sanity checks ───────────────────────── */ marlin() - .arg("search").arg("TODO") + .arg("search") + .arg("TODO") .assert() .stdout(predicate::str::contains("TODO.txt")); marlin() - .arg("search").arg("attr:reviewed=yes") + .arg("search") + .arg("attr:reviewed=yes") .assert() .stdout(predicate::str::contains("Q1.pdf")); @@ -92,31 +92,29 @@ fn full_cli_flow() -> Result<(), Box> { ok(marlin().arg("scan").arg(&demo_dir)); - ok(marlin() - .arg("link").arg("add") - .arg(&foo).arg(&bar)); + ok(marlin().arg("link").arg("add").arg(&foo).arg(&bar)); marlin() - .arg("link").arg("backlinks").arg(&bar) + .arg("link") + .arg("backlinks") + .arg(&bar) .assert() .stdout(predicate::str::contains("foo.txt")); /* ── 6 ░ backup → delete DB → restore ────────────────────── */ - let backup_path = String::from_utf8( - marlin().arg("backup").output()?.stdout - )?; + let backup_path = String::from_utf8(marlin().arg("backup").output()?.stdout)?; let backup_file = backup_path.split_whitespace().last().unwrap(); - fs::remove_file(&db_path)?; // simulate corruption - ok(marlin().arg("restore").arg(backup_file)); // restore + fs::remove_file(&db_path)?; // simulate corruption + ok(marlin().arg("restore").arg(backup_file)); // restore // Search must still work afterwards marlin() - .arg("search").arg("TODO") + .arg("search") + .arg("TODO") .assert() .stdout(predicate::str::contains("TODO.txt")); Ok(()) } - diff --git a/cli-bin/tests/neg.rs b/cli-bin/tests/neg.rs index 23105cd..eb9453b 100644 --- a/cli-bin/tests/neg.rs +++ b/cli-bin/tests/neg.rs @@ -13,7 +13,11 @@ use util::marlin; fn link_non_indexed_should_fail() { let tmp = tempdir().unwrap(); - marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success(); + marlin(&tmp) + .current_dir(tmp.path()) + .arg("init") + .assert() + .success(); std::fs::write(tmp.path().join("foo.txt"), "").unwrap(); std::fs::write(tmp.path().join("bar.txt"), "").unwrap(); @@ -21,9 +25,10 @@ fn link_non_indexed_should_fail() { marlin(&tmp) .current_dir(tmp.path()) .args([ - "link", "add", + "link", + "add", &tmp.path().join("foo.txt").to_string_lossy(), - &tmp.path().join("bar.txt").to_string_lossy() + &tmp.path().join("bar.txt").to_string_lossy(), ]) .assert() .failure() @@ -35,16 +40,19 @@ fn link_non_indexed_should_fail() { #[test] fn attr_set_on_non_indexed_file_should_warn() { let tmp = tempdir().unwrap(); - marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success(); + marlin(&tmp) + .current_dir(tmp.path()) + .arg("init") + .assert() + .success(); let ghost = tmp.path().join("ghost.txt"); std::fs::write(&ghost, "").unwrap(); marlin(&tmp) - .args(["attr","set", - &ghost.to_string_lossy(),"foo","bar"]) + .args(["attr", "set", &ghost.to_string_lossy(), "foo", "bar"]) .assert() - .success() // exits 0 + .success() // exits 0 .stderr(str::contains("not indexed")); } @@ -52,14 +60,18 @@ fn attr_set_on_non_indexed_file_should_warn() { #[test] fn coll_add_unknown_collection_should_fail() { - let tmp = tempdir().unwrap(); + let tmp = tempdir().unwrap(); let file = tmp.path().join("doc.txt"); std::fs::write(&file, "").unwrap(); - marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success(); + marlin(&tmp) + .current_dir(tmp.path()) + .arg("init") + .assert() + .success(); marlin(&tmp) - .args(["coll","add","nope",&file.to_string_lossy()]) + .args(["coll", "add", "nope", &file.to_string_lossy()]) .assert() .failure(); } @@ -68,7 +80,7 @@ fn coll_add_unknown_collection_should_fail() { #[test] fn restore_with_nonexistent_backup_should_fail() { - let tmp = tempdir().unwrap(); + let tmp = tempdir().unwrap(); // create an empty DB first marlin(&tmp).arg("init").assert().success(); @@ -79,4 +91,3 @@ fn restore_with_nonexistent_backup_should_fail() { .failure() .stderr(str::contains("Failed to restore")); } - diff --git a/cli-bin/tests/pos.rs b/cli-bin/tests/pos.rs index a262652..d865be2 100644 --- a/cli-bin/tests/pos.rs +++ b/cli-bin/tests/pos.rs @@ -5,7 +5,7 @@ mod util; use util::marlin; -use predicates::{prelude::*, str}; // brings `PredicateBooleanExt::and` +use predicates::{prelude::*, str}; // brings `PredicateBooleanExt::and` use std::fs; use tempfile::tempdir; @@ -13,15 +13,20 @@ use tempfile::tempdir; #[test] fn tag_should_add_hierarchical_tag_and_search_finds_it() { - let tmp = tempdir().unwrap(); + let tmp = tempdir().unwrap(); let file = tmp.path().join("foo.md"); fs::write(&file, "# test\n").unwrap(); - marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success(); + marlin(&tmp) + .current_dir(tmp.path()) + .arg("init") + .assert() + .success(); marlin(&tmp) .args(["tag", file.to_str().unwrap(), "project/md"]) - .assert().success(); + .assert() + .success(); marlin(&tmp) .args(["search", "tag:project/md"]) @@ -34,15 +39,20 @@ fn tag_should_add_hierarchical_tag_and_search_finds_it() { #[test] fn attr_set_then_ls_roundtrip() { - let tmp = tempdir().unwrap(); + let tmp = tempdir().unwrap(); let file = tmp.path().join("report.pdf"); fs::write(&file, "%PDF-1.4\n").unwrap(); - marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success(); + marlin(&tmp) + .current_dir(tmp.path()) + .arg("init") + .assert() + .success(); marlin(&tmp) .args(["attr", "set", file.to_str().unwrap(), "reviewed", "yes"]) - .assert().success(); + .assert() + .success(); marlin(&tmp) .args(["attr", "ls", file.to_str().unwrap()]) @@ -62,11 +72,21 @@ fn coll_create_add_and_list() { fs::write(&a, "").unwrap(); fs::write(&b, "").unwrap(); - marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success(); + marlin(&tmp) + .current_dir(tmp.path()) + .arg("init") + .assert() + .success(); - marlin(&tmp).args(["coll", "create", "Set"]).assert().success(); + marlin(&tmp) + .args(["coll", "create", "Set"]) + .assert() + .success(); for f in [&a, &b] { - marlin(&tmp).args(["coll", "add", "Set", f.to_str().unwrap()]).assert().success(); + marlin(&tmp) + .args(["coll", "add", "Set", f.to_str().unwrap()]) + .assert() + .success(); } marlin(&tmp) @@ -80,15 +100,22 @@ fn coll_create_add_and_list() { #[test] fn view_save_list_and_exec() { - let tmp = tempdir().unwrap(); + let tmp = tempdir().unwrap(); let todo = tmp.path().join("TODO.txt"); fs::write(&todo, "remember the milk\n").unwrap(); - marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success(); + marlin(&tmp) + .current_dir(tmp.path()) + .arg("init") + .assert() + .success(); // save & list - marlin(&tmp).args(["view", "save", "tasks", "milk"]).assert().success(); + marlin(&tmp) + .args(["view", "save", "tasks", "milk"]) + .assert() + .success(); marlin(&tmp) .args(["view", "list"]) .assert() @@ -118,24 +145,30 @@ fn link_add_rm_and_list() { let mc = || marlin(&tmp); mc().current_dir(tmp.path()).arg("init").assert().success(); - mc().args(["scan", tmp.path().to_str().unwrap()]).assert().success(); + mc().args(["scan", tmp.path().to_str().unwrap()]) + .assert() + .success(); // add mc().args(["link", "add", foo.to_str().unwrap(), bar.to_str().unwrap()]) - .assert().success(); + .assert() + .success(); // list (outgoing default) mc().args(["link", "list", foo.to_str().unwrap()]) - .assert().success() + .assert() + .success() .stdout(str::contains("foo.txt").and(str::contains("bar.txt"))); // remove mc().args(["link", "rm", foo.to_str().unwrap(), bar.to_str().unwrap()]) - .assert().success(); + .assert() + .success(); // list now empty mc().args(["link", "list", foo.to_str().unwrap()]) - .assert().success() + .assert() + .success() .stdout(str::is_empty()); } @@ -154,19 +187,24 @@ fn scan_with_multiple_paths_indexes_all() { fs::write(&f1, "").unwrap(); fs::write(&f2, "").unwrap(); - marlin(&tmp).current_dir(tmp.path()).arg("init").assert().success(); + marlin(&tmp) + .current_dir(tmp.path()) + .arg("init") + .assert() + .success(); // multi-path scan marlin(&tmp) .args(["scan", dir_a.to_str().unwrap(), dir_b.to_str().unwrap()]) - .assert().success(); + .assert() + .success(); // both files findable for term in ["one.txt", "two.txt"] { - marlin(&tmp).args(["search", term]) + marlin(&tmp) + .args(["search", term]) .assert() .success() .stdout(str::contains(term)); } } - diff --git a/cli-bin/tests/util.rs b/cli-bin/tests/util.rs index 5f19ffb..b404866 100644 --- a/cli-bin/tests/util.rs +++ b/cli-bin/tests/util.rs @@ -1,9 +1,9 @@ //! tests/util.rs //! Small helpers shared across integration tests. +use assert_cmd::Command; use std::path::{Path, PathBuf}; use tempfile::TempDir; -use assert_cmd::Command; /// Absolute path to the freshly-built `marlin` binary. pub fn bin() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_marlin")) diff --git a/cli-bin/tests/watch_unit.rs b/cli-bin/tests/watch_unit.rs index 574f714..9230d92 100644 --- a/cli-bin/tests/watch_unit.rs +++ b/cli-bin/tests/watch_unit.rs @@ -2,11 +2,11 @@ use std::thread; use std::time::Duration; use tempfile::tempdir; -use marlin_cli::cli::{watch, Format}; -use marlin_cli::cli::watch::WatchCmd; +use libc; use libmarlin::watcher::WatcherState; use libmarlin::{self as marlin, db}; -use libc; +use marlin_cli::cli::watch::WatchCmd; +use marlin_cli::cli::{watch, Format}; #[test] fn watch_start_and_stop_quickly() { @@ -20,7 +20,10 @@ fn watch_start_and_stop_quickly() { let mut conn = db::open(&db_path).unwrap(); let path = tmp.path().to_path_buf(); - let cmd = WatchCmd::Start { path: path.clone(), debounce_ms: 50 }; + let cmd = WatchCmd::Start { + path: path.clone(), + debounce_ms: 50, + }; // send SIGINT shortly after watcher starts let t = thread::spawn(|| { diff --git a/libmarlin/src/backup.rs b/libmarlin/src/backup.rs index 3fce5ab..393bda4 100644 --- a/libmarlin/src/backup.rs +++ b/libmarlin/src/backup.rs @@ -1,7 +1,7 @@ // libmarlin/src/backup.rs use anyhow::{anyhow, Context, Result}; -use chrono::{DateTime, Local, NaiveDateTime, Utc, TimeZone}; +use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc}; use rusqlite; use std::fs; use std::path::{Path, PathBuf}; @@ -30,7 +30,10 @@ pub struct BackupManager { } impl BackupManager { - pub fn new, P2: AsRef>(live_db_path: P1, backups_dir: P2) -> Result { + pub fn new, P2: AsRef>( + live_db_path: P1, + backups_dir: P2, + ) -> Result { let backups_dir_path = backups_dir.as_ref().to_path_buf(); if !backups_dir_path.exists() { fs::create_dir_all(&backups_dir_path).with_context(|| { @@ -40,7 +43,10 @@ impl BackupManager { ) })?; } else if !backups_dir_path.is_dir() { - return Err(anyhow!("Backups path exists but is not a directory: {}", backups_dir_path.display())); + return Err(anyhow!( + "Backups path exists but is not a directory: {}", + backups_dir_path.display() + )); } Ok(Self { live_db_path: live_db_path.as_ref().to_path_buf(), @@ -54,10 +60,14 @@ impl BackupManager { let backup_file_path = self.backups_dir.join(&backup_file_name); if !self.live_db_path.exists() { - return Err(anyhow::Error::new(std::io::Error::new( + return Err(anyhow::Error::new(std::io::Error::new( std::io::ErrorKind::NotFound, - format!("Live DB path does not exist: {}", self.live_db_path.display()), - )).context("Cannot create backup from non-existent live DB")); + format!( + "Live DB path does not exist: {}", + self.live_db_path.display() + ), + )) + .context("Cannot create backup from non-existent live DB")); } let src_conn = rusqlite::Connection::open_with_flags( @@ -108,8 +118,8 @@ impl BackupManager { pub fn list_backups(&self) -> Result> { let mut backup_infos = Vec::new(); - - if !self.backups_dir.exists() { + + if !self.backups_dir.exists() { return Ok(backup_infos); } @@ -129,28 +139,42 @@ impl BackupManager { let ts_str = filename .trim_start_matches("backup_") .trim_end_matches(".db"); - - let naive_dt = match NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%d_%H-%M-%S_%f") { - Ok(dt) => dt, - Err(_) => match NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%d_%H-%M-%S") { + + let naive_dt = + match NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%d_%H-%M-%S_%f") + { Ok(dt) => dt, - Err(_) => { - let metadata = fs::metadata(&path).with_context(|| format!("Failed to get metadata for {}", path.display()))?; - DateTime::::from(metadata.modified()?).naive_utc() - } - } - }; - + Err(_) => match NaiveDateTime::parse_from_str( + ts_str, + "%Y-%m-%d_%H-%M-%S", + ) { + Ok(dt) => dt, + Err(_) => { + let metadata = + fs::metadata(&path).with_context(|| { + format!( + "Failed to get metadata for {}", + path.display() + ) + })?; + DateTime::::from(metadata.modified()?).naive_utc() + } + }, + }; + let local_dt_result = Local.from_local_datetime(&naive_dt); let local_dt = match local_dt_result { chrono::LocalResult::Single(dt) => dt, chrono::LocalResult::Ambiguous(dt1, _dt2) => { eprintln!("Warning: Ambiguous local time for backup {}, taking first interpretation.", filename); dt1 - }, + } chrono::LocalResult::None => { - eprintln!("Warning: Invalid local time for backup {}, skipping.", filename); - continue; + eprintln!( + "Warning: Invalid local time for backup {}, skipping.", + filename + ); + continue; } }; let timestamp_utc = DateTime::::from(local_dt); @@ -172,12 +196,12 @@ impl BackupManager { } pub fn prune(&self, keep_count: usize) -> Result { - let all_backups = self.list_backups()?; + let all_backups = self.list_backups()?; let mut kept = Vec::new(); let mut removed = Vec::new(); - if keep_count >= all_backups.len() { + if keep_count >= all_backups.len() { kept = all_backups; } else { for (index, backup_info) in all_backups.into_iter().enumerate() { @@ -185,7 +209,7 @@ impl BackupManager { kept.push(backup_info); } else { let backup_file_path = self.backups_dir.join(&backup_info.id); - if backup_file_path.exists() { + if backup_file_path.exists() { fs::remove_file(&backup_file_path).with_context(|| { format!( "Failed to remove old backup file: {}", @@ -223,16 +247,22 @@ impl BackupManager { #[cfg(test)] mod tests { use super::*; - use tempfile::tempdir; use crate::db::open as open_marlin_db; + use tempfile::tempdir; fn create_valid_live_db(path: &Path) -> rusqlite::Connection { - let conn = open_marlin_db(path) - .unwrap_or_else(|e| panic!("Failed to open/create test DB at {}: {:?}", path.display(), e)); + let conn = open_marlin_db(path).unwrap_or_else(|e| { + panic!( + "Failed to open/create test DB at {}: {:?}", + path.display(), + e + ) + }); conn.execute_batch( "CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY, data TEXT); - INSERT INTO test_table (data) VALUES ('initial_data');" - ).expect("Failed to initialize test table"); + INSERT INTO test_table (data) VALUES ('initial_data');", + ) + .expect("Failed to initialize test table"); conn } @@ -246,7 +276,7 @@ mod tests { assert!(!backups_dir.exists()); let manager = BackupManager::new(&live_db_path, &backups_dir).unwrap(); - assert!(manager.backups_dir.exists()); + assert!(manager.backups_dir.exists()); assert!(backups_dir.exists()); } @@ -257,7 +287,7 @@ mod tests { let _conn = create_valid_live_db(&live_db_path); let backups_dir = base_tmp.path().join("my_backups_existing_test"); - std::fs::create_dir_all(&backups_dir).unwrap(); + std::fs::create_dir_all(&backups_dir).unwrap(); assert!(backups_dir.exists()); let manager_res = BackupManager::new(&live_db_path, &backups_dir); @@ -265,7 +295,7 @@ mod tests { let manager = manager_res.unwrap(); assert_eq!(manager.backups_dir, backups_dir); } - + #[test] fn test_backup_manager_new_fails_if_backup_path_is_file() { let base_tmp = tempdir().unwrap(); @@ -276,20 +306,26 @@ mod tests { let manager_res = BackupManager::new(&live_db_path, &file_as_backups_dir); assert!(manager_res.is_err()); - assert!(manager_res.unwrap_err().to_string().contains("Backups path exists but is not a directory")); + assert!(manager_res + .unwrap_err() + .to_string() + .contains("Backups path exists but is not a directory")); } #[test] fn test_create_backup_failure_non_existent_live_db() { let base_tmp = tempdir().unwrap(); - let live_db_path = base_tmp.path().join("non_existent_live.db"); + let live_db_path = base_tmp.path().join("non_existent_live.db"); let backups_dir = base_tmp.path().join("backups_fail_test"); let manager = BackupManager::new(&live_db_path, &backups_dir).unwrap(); let backup_result = manager.create_backup(); assert!(backup_result.is_err()); let err_str = backup_result.unwrap_err().to_string(); - assert!(err_str.contains("Cannot create backup from non-existent live DB") || err_str.contains("Failed to open source DB")); + assert!( + err_str.contains("Cannot create backup from non-existent live DB") + || err_str.contains("Failed to open source DB") + ); } #[test] @@ -299,11 +335,14 @@ mod tests { let _conn_live = create_valid_live_db(&live_db_file); let backups_storage_dir = tmp.path().join("backups_clp_storage_test"); - + let manager = BackupManager::new(&live_db_file, &backups_storage_dir).unwrap(); let initial_list = manager.list_backups().unwrap(); - assert!(initial_list.is_empty(), "Backup list should be empty initially"); + assert!( + initial_list.is_empty(), + "Backup list should be empty initially" + ); let prune_empty_result = manager.prune(2).unwrap(); assert!(prune_empty_result.kept.is_empty()); @@ -314,7 +353,7 @@ mod tests { let info = manager .create_backup() .unwrap_or_else(|e| panic!("Failed to create backup {}: {:?}", i, e)); - created_backup_ids.push(info.id.clone()); + created_backup_ids.push(info.id.clone()); std::thread::sleep(std::time::Duration::from_millis(30)); } @@ -323,7 +362,8 @@ mod tests { for id in &created_backup_ids { assert!( listed_backups.iter().any(|b| &b.id == id), - "Backup ID {} not found in list", id + "Backup ID {} not found in list", + id ); } if listed_backups.len() >= 2 { @@ -337,7 +377,7 @@ mod tests { assert!(listed_after_prune_zero.is_empty()); created_backup_ids.clear(); - for i in 0..5 { + for i in 0..5 { let info = manager .create_backup() .unwrap_or_else(|e| panic!("Failed to create backup {}: {:?}", i, e)); @@ -360,31 +400,34 @@ mod tests { assert_eq!(listed_after_prune[0].id, created_backup_ids[4]); assert_eq!(listed_after_prune[1].id, created_backup_ids[3]); - + for removed_info in prune_result.removed { assert!( !backups_storage_dir.join(&removed_info.id).exists(), - "Removed backup file {} should not exist", removed_info.id + "Removed backup file {} should not exist", + removed_info.id ); } for kept_info in prune_result.kept { assert!( backups_storage_dir.join(&kept_info.id).exists(), - "Kept backup file {} should exist", kept_info.id + "Kept backup file {} should exist", + kept_info.id ); } } - #[test] + #[test] fn test_restore_backup() { let tmp = tempdir().unwrap(); let live_db_path = tmp.path().join("live_for_restore_test.db"); - + let initial_value = "initial_data_for_restore"; { let conn = create_valid_live_db(&live_db_path); - conn.execute("DELETE FROM test_table", []).unwrap(); - conn.execute("INSERT INTO test_table (data) VALUES (?1)", [initial_value]).unwrap(); + conn.execute("DELETE FROM test_table", []).unwrap(); + conn.execute("INSERT INTO test_table (data) VALUES (?1)", [initial_value]) + .unwrap(); } let backups_dir = tmp.path().join("backups_for_restore_test_dir"); @@ -403,7 +446,7 @@ mod tests { .unwrap(); assert_eq!(modified_check, modified_value); } - + manager.restore_from_backup(&backup_info.id).unwrap(); { @@ -428,7 +471,11 @@ mod tests { let result = manager.restore_from_backup("non_existent_backup.db"); assert!(result.is_err()); let err_string = result.unwrap_err().to_string(); - assert!(err_string.contains("Backup file not found"), "Error string was: {}", err_string); + assert!( + err_string.contains("Backup file not found"), + "Error string was: {}", + err_string + ); } #[test] @@ -437,17 +484,13 @@ mod tests { let live_db_file = tmp.path().join("live_for_list_test.db"); let _conn = create_valid_live_db(&live_db_file); let backups_dir = tmp.path().join("backups_list_mixed_files_test"); - + let manager = BackupManager::new(&live_db_file, &backups_dir).unwrap(); - manager.create_backup().unwrap(); - + manager.create_backup().unwrap(); + std::fs::write(backups_dir.join("not_a_backup.txt"), "hello").unwrap(); - std::fs::write( - backups_dir.join("backup_malformed.db.tmp"), - "temp data", - ) - .unwrap(); + std::fs::write(backups_dir.join("backup_malformed.db.tmp"), "temp data").unwrap(); std::fs::create_dir(backups_dir.join("a_subdir")).unwrap(); let listed_backups = manager.list_backups().unwrap(); @@ -460,15 +503,16 @@ mod tests { assert!(listed_backups[0].id.ends_with(".db")); } - #[test] + #[test] fn list_backups_handles_io_error_on_read_dir() { let tmp = tempdir().unwrap(); let live_db_file = tmp.path().join("live_for_list_io_error.db"); let _conn = create_valid_live_db(&live_db_file); - + let backups_dir_for_deletion = tmp.path().join("backups_dir_to_delete_test"); - let manager_for_deletion = BackupManager::new(&live_db_file, &backups_dir_for_deletion).unwrap(); - std::fs::remove_dir_all(&backups_dir_for_deletion).unwrap(); + let manager_for_deletion = + BackupManager::new(&live_db_file, &backups_dir_for_deletion).unwrap(); + std::fs::remove_dir_all(&backups_dir_for_deletion).unwrap(); let list_res = manager_for_deletion.list_backups().unwrap(); assert!(list_res.is_empty()); diff --git a/libmarlin/src/db/database.rs b/libmarlin/src/db/database.rs index c609bbb..59a113a 100644 --- a/libmarlin/src/db/database.rs +++ b/libmarlin/src/db/database.rs @@ -1,21 +1,21 @@ //! Database abstraction for Marlin -//! +//! //! This module provides a database abstraction layer that wraps the SQLite connection //! and provides methods for common database operations. +use anyhow::Result; use rusqlite::Connection; use std::path::PathBuf; -use anyhow::Result; /// Options for indexing files #[derive(Debug, Clone)] pub struct IndexOptions { /// Only update files marked as dirty pub dirty_only: bool, - + /// Index file contents (not just metadata) pub index_contents: bool, - + /// Maximum file size to index (in bytes) pub max_size: Option, } @@ -41,32 +41,34 @@ impl Database { pub fn new(conn: Connection) -> Self { Self { conn } } - + /// Get a reference to the underlying connection pub fn conn(&self) -> &Connection { &self.conn } - + /// Get a mutable reference to the underlying connection pub fn conn_mut(&mut self) -> &mut Connection { &mut self.conn } - + /// Index one or more files pub fn index_files(&mut self, paths: &[PathBuf], _options: &IndexOptions) -> Result { // In a real implementation, this would index the files // For now, we just return the number of files "indexed" - if paths.is_empty() { // Add a branch for coverage + if paths.is_empty() { + // Add a branch for coverage return Ok(0); } Ok(paths.len()) } - + /// Remove files from the index pub fn remove_files(&mut self, paths: &[PathBuf]) -> Result { // In a real implementation, this would remove the files // For now, we just return the number of files "removed" - if paths.is_empty() { // Add a branch for coverage + if paths.is_empty() { + // Add a branch for coverage return Ok(0); } Ok(paths.len()) @@ -77,8 +79,8 @@ impl Database { mod tests { use super::*; use crate::db::open as open_marlin_db; // Use your project's DB open function - use tempfile::tempdir; use std::fs::File; + use tempfile::tempdir; fn setup_db() -> Database { let conn = open_marlin_db(":memory:").expect("Failed to open in-memory DB"); @@ -102,7 +104,7 @@ mod tests { let paths = vec![file1.to_path_buf()]; let options = IndexOptions::default(); - + assert_eq!(db.index_files(&paths, &options).unwrap(), 1); assert_eq!(db.index_files(&[], &options).unwrap(), 0); // Test empty case } @@ -115,7 +117,7 @@ mod tests { File::create(&file1).unwrap(); // File doesn't need to be in DB for this stub let paths = vec![file1.to_path_buf()]; - + assert_eq!(db.remove_files(&paths).unwrap(), 1); assert_eq!(db.remove_files(&[]).unwrap(), 0); // Test empty case } diff --git a/libmarlin/src/db/mod.rs b/libmarlin/src/db/mod.rs index 767b172..fc7974d 100644 --- a/libmarlin/src/db/mod.rs +++ b/libmarlin/src/db/mod.rs @@ -9,27 +9,38 @@ use std::{ path::{Path, PathBuf}, }; -use std::result::Result as StdResult; use anyhow::{Context, Result}; use chrono::Local; use rusqlite::{ backup::{Backup, StepResult}, - params, - Connection, - OpenFlags, - OptionalExtension, - TransactionBehavior, + params, Connection, OpenFlags, OptionalExtension, TransactionBehavior, }; +use std::result::Result as StdResult; use tracing::{debug, info, warn}; /* ─── embedded migrations ─────────────────────────────────────────── */ const MIGRATIONS: &[(&str, &str)] = &[ - ("0001_initial_schema.sql", include_str!("migrations/0001_initial_schema.sql")), - ("0002_update_fts_and_triggers.sql", include_str!("migrations/0002_update_fts_and_triggers.sql")), - ("0003_create_links_collections_views.sql", include_str!("migrations/0003_create_links_collections_views.sql")), - ("0004_fix_hierarchical_tags_fts.sql", include_str!("migrations/0004_fix_hierarchical_tags_fts.sql")), - ("0005_add_dirty_table.sql", include_str!("migrations/0005_add_dirty_table.sql")), + ( + "0001_initial_schema.sql", + include_str!("migrations/0001_initial_schema.sql"), + ), + ( + "0002_update_fts_and_triggers.sql", + include_str!("migrations/0002_update_fts_and_triggers.sql"), + ), + ( + "0003_create_links_collections_views.sql", + include_str!("migrations/0003_create_links_collections_views.sql"), + ), + ( + "0004_fix_hierarchical_tags_fts.sql", + include_str!("migrations/0004_fix_hierarchical_tags_fts.sql"), + ), + ( + "0005_add_dirty_table.sql", + include_str!("migrations/0005_add_dirty_table.sql"), + ), ]; /* ─── connection bootstrap ────────────────────────────────────────── */ @@ -237,10 +248,7 @@ pub fn list_links( Ok(out) } -pub fn find_backlinks( - conn: &Connection, - pattern: &str, -) -> Result)>> { +pub fn find_backlinks(conn: &Connection, pattern: &str) -> Result)>> { let like = pattern.replace('*', "%"); let mut stmt = conn.prepare( @@ -318,11 +326,9 @@ pub fn list_views(conn: &Connection) -> Result> { } pub fn view_query(conn: &Connection, name: &str) -> Result { - conn.query_row( - "SELECT query FROM views WHERE name = ?1", - [name], - |r| r.get::<_, String>(0), - ) + conn.query_row("SELECT query FROM views WHERE name = ?1", [name], |r| { + r.get::<_, String>(0) + }) .context(format!("no view called '{}'", name)) } diff --git a/libmarlin/src/db_tests.rs b/libmarlin/src/db_tests.rs index 0dc2c28..29c4e69 100644 --- a/libmarlin/src/db_tests.rs +++ b/libmarlin/src/db_tests.rs @@ -90,7 +90,9 @@ fn file_id_returns_id_and_errors_on_missing() { // fetch its id via raw SQL let fid: i64 = conn - .query_row("SELECT id FROM files WHERE path='exist.txt'", [], |r| r.get(0)) + .query_row("SELECT id FROM files WHERE path='exist.txt'", [], |r| { + r.get(0) + }) .unwrap(); // db::file_id should return the same id for existing paths @@ -116,10 +118,14 @@ fn add_and_remove_links_and_backlinks() { ) .unwrap(); let src: i64 = conn - .query_row("SELECT id FROM files WHERE path='one.txt'", [], |r| r.get(0)) + .query_row("SELECT id FROM files WHERE path='one.txt'", [], |r| { + r.get(0) + }) .unwrap(); let dst: i64 = conn - .query_row("SELECT id FROM files WHERE path='two.txt'", [], |r| r.get(0)) + .query_row("SELECT id FROM files WHERE path='two.txt'", [], |r| { + r.get(0) + }) .unwrap(); // add a link of type "ref" @@ -193,8 +199,11 @@ fn backup_and_restore_cycle() { // reopen and check that x.bin survived let conn2 = db::open(&db_path).unwrap(); - let cnt: i64 = - conn2.query_row("SELECT COUNT(*) FROM files WHERE path='x.bin'", [], |r| r.get(0)).unwrap(); + let cnt: i64 = conn2 + .query_row("SELECT COUNT(*) FROM files WHERE path='x.bin'", [], |r| { + r.get(0) + }) + .unwrap(); assert_eq!(cnt, 1); } @@ -210,7 +219,9 @@ mod dirty_helpers { ) .unwrap(); let fid: i64 = conn - .query_row("SELECT id FROM files WHERE path='dummy.txt'", [], |r| r.get(0)) + .query_row("SELECT id FROM files WHERE path='dummy.txt'", [], |r| { + r.get(0) + }) .unwrap(); db::mark_dirty(&conn, fid).unwrap(); diff --git a/libmarlin/src/error.rs b/libmarlin/src/error.rs index 50f3ece..329d05c 100644 --- a/libmarlin/src/error.rs +++ b/libmarlin/src/error.rs @@ -1,7 +1,7 @@ // libmarlin/src/error.rs -use std::io; use std::fmt; +use std::io; // Ensure these are present if Error enum variants use them directly // use rusqlite; // use notify; @@ -11,8 +11,8 @@ pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { Io(io::Error), - Database(rusqlite::Error), - Watch(notify::Error), + Database(rusqlite::Error), + Watch(notify::Error), InvalidState(String), NotFound(String), Config(String), @@ -65,12 +65,13 @@ impl From for Error { #[cfg(test)] mod tests { use super::*; - use std::error::Error as StdError; + use std::error::Error as StdError; #[test] fn test_error_display_and_from() { // Test Io variant - let io_err_inner_for_source_check = io::Error::new(io::ErrorKind::NotFound, "test io error"); + let io_err_inner_for_source_check = + io::Error::new(io::ErrorKind::NotFound, "test io error"); let io_err_marlin = Error::from(io::Error::new(io::ErrorKind::NotFound, "test io error")); assert_eq!(io_err_marlin.to_string(), "IO error: test io error"); let source = io_err_marlin.source(); @@ -82,33 +83,44 @@ mod tests { // Test Database variant let rusqlite_err_inner_for_source_check = rusqlite::Error::SqliteFailure( - rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR), + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR), Some("test db error".to_string()), ); // We need to create the error again for the From conversion if we want to compare the source let db_err_marlin = Error::from(rusqlite::Error::SqliteFailure( - rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR), + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR), Some("test db error".to_string()), )); - assert!(db_err_marlin.to_string().contains("Database error: test db error")); + assert!(db_err_marlin + .to_string() + .contains("Database error: test db error")); let source = db_err_marlin.source(); assert!(source.is_some(), "Database error should have a source"); if let Some(s) = source { - assert_eq!(s.to_string(), rusqlite_err_inner_for_source_check.to_string()); + assert_eq!( + s.to_string(), + rusqlite_err_inner_for_source_check.to_string() + ); } - // Test Watch variant - let notify_raw_err_inner_for_source_check = notify::Error::new(notify::ErrorKind::Generic("test watch error".to_string())); - let watch_err_marlin = Error::from(notify::Error::new(notify::ErrorKind::Generic("test watch error".to_string()))); - assert!(watch_err_marlin.to_string().contains("Watch error: test watch error")); + let notify_raw_err_inner_for_source_check = + notify::Error::new(notify::ErrorKind::Generic("test watch error".to_string())); + let watch_err_marlin = Error::from(notify::Error::new(notify::ErrorKind::Generic( + "test watch error".to_string(), + ))); + assert!(watch_err_marlin + .to_string() + .contains("Watch error: test watch error")); let source = watch_err_marlin.source(); assert!(source.is_some(), "Watch error should have a source"); if let Some(s) = source { - assert_eq!(s.to_string(), notify_raw_err_inner_for_source_check.to_string()); + assert_eq!( + s.to_string(), + notify_raw_err_inner_for_source_check.to_string() + ); } - let invalid_state_err = Error::InvalidState("bad state".to_string()); assert_eq!(invalid_state_err.to_string(), "Invalid state: bad state"); assert!(invalid_state_err.source().is_none()); @@ -133,24 +145,25 @@ mod tests { None, ); let db_err_no_msg = Error::from(sqlite_busy_error); - + let expected_rusqlite_msg = rusqlite::Error::SqliteFailure( rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY), None, - ).to_string(); - + ) + .to_string(); + let expected_marlin_msg = format!("Database error: {}", expected_rusqlite_msg); - + // Verify the string matches the expected format assert_eq!(db_err_no_msg.to_string(), expected_marlin_msg); - + // Check the error code directly instead of the string if let Error::Database(rusqlite::Error::SqliteFailure(err, _)) = &db_err_no_msg { assert_eq!(err.code, rusqlite::ffi::ErrorCode::DatabaseBusy); } else { panic!("Expected Error::Database variant"); } - + // Verify the source exists assert!(db_err_no_msg.source().is_some()); } diff --git a/libmarlin/src/facade_tests.rs b/libmarlin/src/facade_tests.rs index 2244f05..2e3f259 100644 --- a/libmarlin/src/facade_tests.rs +++ b/libmarlin/src/facade_tests.rs @@ -1,6 +1,6 @@ // libmarlin/src/facade_tests.rs -use super::*; // brings Marlin, config, etc. +use super::*; // brings Marlin, config, etc. use std::{env, fs}; use tempfile::tempdir; @@ -71,4 +71,3 @@ fn open_default_fallback_config() { // Clean up env::remove_var("HOME"); } - diff --git a/libmarlin/src/lib.rs b/libmarlin/src/lib.rs index 7bd52c3..ff19a88 100644 --- a/libmarlin/src/lib.rs +++ b/libmarlin/src/lib.rs @@ -16,24 +16,28 @@ pub mod scan; pub mod utils; pub mod watcher; -#[cfg(test)] -mod utils_tests; #[cfg(test)] mod config_tests; #[cfg(test)] -mod scan_tests; -#[cfg(test)] -mod logging_tests; -#[cfg(test)] mod db_tests; #[cfg(test)] mod facade_tests; #[cfg(test)] +mod logging_tests; +#[cfg(test)] +mod scan_tests; +#[cfg(test)] +mod utils_tests; +#[cfg(test)] mod watcher_tests; use anyhow::{Context, Result}; use rusqlite::Connection; -use std::{fs, path::Path, sync::{Arc, Mutex}}; +use std::{ + fs, + path::Path, + sync::{Arc, Mutex}, +}; /// Main handle for interacting with a Marlin database. pub struct Marlin { @@ -66,10 +70,12 @@ impl Marlin { fs::create_dir_all(parent)?; } // Build a minimal Config so callers can still inspect cfg.db_path - let cfg = config::Config { db_path: db_path.to_path_buf() }; + let cfg = config::Config { + db_path: db_path.to_path_buf(), + }; // Open the database and run migrations - let conn = db::open(db_path) - .context(format!("opening database at {}", db_path.display()))?; + let conn = + db::open(db_path).context(format!("opening database at {}", db_path.display()))?; Ok(Marlin { cfg, conn }) } @@ -95,11 +101,11 @@ impl Marlin { let mut cur = Some(leaf); while let Some(id) = cur { tag_ids.push(id); - cur = self.conn.query_row( - "SELECT parent_id FROM tags WHERE id = ?1", - [id], - |r| r.get::<_, Option>(0), - )?; + cur = self + .conn + .query_row("SELECT parent_id FROM tags WHERE id = ?1", [id], |r| { + r.get::<_, Option>(0) + })?; } // 3) match files by glob against stored paths @@ -110,9 +116,9 @@ impl Marlin { let mut stmt_all = self.conn.prepare("SELECT id, path FROM files")?; let rows = stmt_all.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?; - let mut stmt_ins = self.conn.prepare( - "INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)", - )?; + let mut stmt_ins = self + .conn + .prepare("INSERT OR IGNORE INTO file_tags(file_id, tag_id) VALUES (?1, ?2)")?; let mut changed = 0; for row in rows { @@ -148,7 +154,8 @@ impl Marlin { let mut stmt = self.conn.prepare( "SELECT f.path FROM files_fts JOIN files f ON f.rowid = files_fts.rowid WHERE files_fts MATCH ?1 ORDER BY rank", )?; - let mut hits = stmt.query_map([query], |r| r.get(0))? + let mut hits = stmt + .query_map([query], |r| r.get(0))? .collect::, rusqlite::Error>>()?; if hits.is_empty() && !query.contains(':') { @@ -169,7 +176,7 @@ impl Marlin { continue; } if let Ok(meta) = fs::metadata(&p) { - if meta.len() <= 65_536 { + if meta.len() <= 65_536 { if let Ok(body) = fs::read_to_string(&p) { if body.to_lowercase().contains(&needle) { out.push(p.clone()); @@ -194,14 +201,13 @@ impl Marlin { ) -> Result { let cfg = config.unwrap_or_default(); let p = path.as_ref().to_path_buf(); - let new_conn = db::open(&self.cfg.db_path) - .context("opening database for watcher")?; + let new_conn = db::open(&self.cfg.db_path).context("opening database for watcher")?; let watcher_db = Arc::new(Mutex::new(db::Database::new(new_conn))); - + let mut owned_w = watcher::FileWatcher::new(vec![p], cfg)?; owned_w.with_database(watcher_db)?; // Modifies owned_w in place owned_w.start()?; // Start the watcher after it has been fully configured - + Ok(owned_w) // Return the owned FileWatcher } } diff --git a/libmarlin/src/logging.rs b/libmarlin/src/logging.rs index 514fa0d..e425217 100644 --- a/libmarlin/src/logging.rs +++ b/libmarlin/src/logging.rs @@ -9,9 +9,9 @@ pub fn init() { // All tracing output (INFO, WARN, ERROR …) now goes to *stderr* so the // integration tests can assert on warnings / errors reliably. fmt() - .with_target(false) // hide module targets - .with_level(true) // include log level - .with_env_filter(filter) // respect RUST_LOG + .with_target(false) // hide module targets + .with_level(true) // include log level + .with_env_filter(filter) // respect RUST_LOG .with_writer(std::io::stderr) // <-- NEW: send to stderr .init(); } diff --git a/libmarlin/src/scan_tests.rs b/libmarlin/src/scan_tests.rs index f9c0855..506aff2 100644 --- a/libmarlin/src/scan_tests.rs +++ b/libmarlin/src/scan_tests.rs @@ -1,9 +1,9 @@ // libmarlin/src/scan_tests.rs -use super::scan::scan_directory; use super::db; -use tempfile::tempdir; +use super::scan::scan_directory; use std::fs::File; +use tempfile::tempdir; #[test] fn scan_directory_counts_files() { diff --git a/libmarlin/src/utils.rs b/libmarlin/src/utils.rs index 8518722..bb3de93 100644 --- a/libmarlin/src/utils.rs +++ b/libmarlin/src/utils.rs @@ -21,7 +21,10 @@ pub fn determine_scan_root(pattern: &str) -> PathBuf { // If there were NO wildcards at all, just return the parent directory if first_wild == pattern.len() { - return root.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| PathBuf::from(".")); + return root + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")); } // Otherwise, if the prefix still has any wildcards (e.g. "foo*/bar"), diff --git a/libmarlin/src/watcher.rs b/libmarlin/src/watcher.rs index 797c93c..c8aad43 100644 --- a/libmarlin/src/watcher.rs +++ b/libmarlin/src/watcher.rs @@ -6,8 +6,8 @@ //! (create, modify, delete) using the `notify` crate. It implements event debouncing, //! batch processing, and a state machine for robust lifecycle management. -use anyhow::{Result, Context}; use crate::db::Database; +use anyhow::{Context, Result}; use crossbeam_channel::{bounded, Receiver}; use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcherTrait}; use std::collections::HashMap; @@ -98,9 +98,11 @@ impl EventDebouncer { fn add_event(&mut self, event: ProcessedEvent) { let path = event.path.clone(); - if path.is_dir() { // This relies on the PathBuf itself knowing if it's a directory - // or on the underlying FS. For unit tests, ensure paths are created. - self.events.retain(|file_path, _| !file_path.starts_with(&path) || file_path == &path ); + if path.is_dir() { + // This relies on the PathBuf itself knowing if it's a directory + // or on the underlying FS. For unit tests, ensure paths are created. + self.events + .retain(|file_path, _| !file_path.starts_with(&path) || file_path == &path); } match self.events.get_mut(&path) { Some(existing) => { @@ -137,12 +139,12 @@ mod event_debouncer_tests { use super::*; use notify::event::{CreateKind, DataChange, ModifyKind, RemoveKind, RenameMode}; use std::fs; // fs is needed for these tests to create dirs/files - use tempfile; + use tempfile; #[test] fn debouncer_add_and_flush() { let mut debouncer = EventDebouncer::new(100); - std::thread::sleep(Duration::from_millis(110)); + std::thread::sleep(Duration::from_millis(110)); assert!(debouncer.is_ready_to_flush()); assert_eq!(debouncer.len(), 0); @@ -154,8 +156,8 @@ mod event_debouncer_tests { timestamp: Instant::now(), }); assert_eq!(debouncer.len(), 1); - - debouncer.last_flush = Instant::now(); + + debouncer.last_flush = Instant::now(); assert!(!debouncer.is_ready_to_flush()); std::thread::sleep(Duration::from_millis(110)); @@ -165,7 +167,7 @@ mod event_debouncer_tests { assert_eq!(flushed.len(), 1); assert_eq!(flushed[0].path, path1); assert_eq!(debouncer.len(), 0); - assert!(!debouncer.is_ready_to_flush()); + assert!(!debouncer.is_ready_to_flush()); } #[test] @@ -188,15 +190,15 @@ mod event_debouncer_tests { priority: EventPriority::Modify, timestamp: t2, }); - + assert_eq!(debouncer.len(), 1); - + std::thread::sleep(Duration::from_millis(110)); let flushed = debouncer.flush(); assert_eq!(flushed.len(), 1); assert_eq!(flushed[0].path, path1); - assert_eq!(flushed[0].priority, EventPriority::Create); - assert_eq!( + assert_eq!(flushed[0].priority, EventPriority::Create); + assert_eq!( flushed[0].kind, EventKind::Modify(ModifyKind::Data(DataChange::Any)) ); @@ -207,9 +209,9 @@ mod event_debouncer_tests { fn debouncer_hierarchical() { let mut debouncer_h = EventDebouncer::new(100); let temp_dir_obj = tempfile::tempdir().expect("Failed to create temp dir"); - let p_dir = temp_dir_obj.path().to_path_buf(); + let p_dir = temp_dir_obj.path().to_path_buf(); let p_file = p_dir.join("file.txt"); - + fs::File::create(&p_file).expect("Failed to create test file for hierarchical debounce"); debouncer_h.add_event(ProcessedEvent { @@ -219,15 +221,19 @@ mod event_debouncer_tests { timestamp: Instant::now(), }); assert_eq!(debouncer_h.len(), 1); - + debouncer_h.add_event(ProcessedEvent { - path: p_dir.clone(), - kind: EventKind::Remove(RemoveKind::Folder), + path: p_dir.clone(), + kind: EventKind::Remove(RemoveKind::Folder), priority: EventPriority::Delete, timestamp: Instant::now(), }); - assert_eq!(debouncer_h.len(), 1, "Hierarchical debounce should remove child event, leaving only parent dir event"); - + assert_eq!( + debouncer_h.len(), + 1, + "Hierarchical debounce should remove child event, leaving only parent dir event" + ); + std::thread::sleep(Duration::from_millis(110)); let flushed = debouncer_h.flush(); assert_eq!(flushed.len(), 1); @@ -261,20 +267,35 @@ mod event_debouncer_tests { #[test] fn debouncer_priority_sorting_on_flush() { let mut debouncer = EventDebouncer::new(100); - let path1 = PathBuf::from("file1.txt"); - let path2 = PathBuf::from("file2.txt"); - let path3 = PathBuf::from("file3.txt"); + let path1 = PathBuf::from("file1.txt"); + let path2 = PathBuf::from("file2.txt"); + let path3 = PathBuf::from("file3.txt"); + + debouncer.add_event(ProcessedEvent { + path: path1, + kind: EventKind::Modify(ModifyKind::Name(RenameMode::To)), + priority: EventPriority::Modify, + timestamp: Instant::now(), + }); + debouncer.add_event(ProcessedEvent { + path: path2, + kind: EventKind::Create(CreateKind::File), + priority: EventPriority::Create, + timestamp: Instant::now(), + }); + debouncer.add_event(ProcessedEvent { + path: path3, + kind: EventKind::Remove(RemoveKind::File), + priority: EventPriority::Delete, + timestamp: Instant::now(), + }); - debouncer.add_event(ProcessedEvent { path: path1, kind: EventKind::Modify(ModifyKind::Name(RenameMode::To)), priority: EventPriority::Modify, timestamp: Instant::now() }); - debouncer.add_event(ProcessedEvent { path: path2, kind: EventKind::Create(CreateKind::File), priority: EventPriority::Create, timestamp: Instant::now() }); - debouncer.add_event(ProcessedEvent { path: path3, kind: EventKind::Remove(RemoveKind::File), priority: EventPriority::Delete, timestamp: Instant::now() }); - std::thread::sleep(Duration::from_millis(110)); let flushed = debouncer.flush(); assert_eq!(flushed.len(), 3); - assert_eq!(flushed[0].priority, EventPriority::Create); - assert_eq!(flushed[1].priority, EventPriority::Delete); - assert_eq!(flushed[2].priority, EventPriority::Modify); + assert_eq!(flushed[0].priority, EventPriority::Create); + assert_eq!(flushed[1].priority, EventPriority::Delete); + assert_eq!(flushed[2].priority, EventPriority::Modify); } #[test] @@ -314,7 +335,6 @@ mod event_debouncer_tests { } } - pub struct FileWatcher { state: Arc>, _config: WatcherConfig, @@ -359,7 +379,7 @@ impl FileWatcher { let events_processed_clone = events_processed.clone(); let queue_size_clone = queue_size.clone(); let state_clone = state.clone(); - let receiver_clone = rx.clone(); + let receiver_clone = rx.clone(); let db_shared_for_thread = Arc::new(Mutex::new(None::>>)); let db_captured_for_thread = db_shared_for_thread.clone(); @@ -367,7 +387,7 @@ impl FileWatcher { let processor_thread = thread::spawn(move || { let mut debouncer = EventDebouncer::new(config_clone.debounce_ms); - while !stop_flag_clone.load(Ordering::Relaxed) { + while !stop_flag_clone.load(Ordering::Relaxed) { let current_state = match state_clone.lock() { Ok(g) => g.clone(), Err(_) => { @@ -380,13 +400,15 @@ impl FileWatcher { thread::sleep(Duration::from_millis(100)); continue; } - if current_state == WatcherState::ShuttingDown || current_state == WatcherState::Stopped { + if current_state == WatcherState::ShuttingDown + || current_state == WatcherState::Stopped + { break; } let mut received_in_batch = 0; while let Ok(evt_res) = receiver_clone.try_recv() { - received_in_batch +=1; + received_in_batch += 1; match evt_res { Ok(event) => { for path in event.paths { @@ -431,7 +453,7 @@ impl FileWatcher { if let Some(db_mutex) = &*db_guard_option { if let Ok(mut _db_instance_guard) = db_mutex.lock() { for event_item in &evts_to_process { - info!( + info!( "Processing event (DB available): {:?} for path {:?}", event_item.kind, event_item.path ); @@ -441,7 +463,7 @@ impl FileWatcher { } } else { for event_item in &evts_to_process { - info!( + info!( "Processing event (no DB): {:?} for path {:?}", event_item.kind, event_item.path ); @@ -504,12 +526,18 @@ impl FileWatcher { return Err(anyhow::anyhow!("Watcher thread not available to start.")); } if *state_guard == WatcherState::Initializing { - *state_guard = WatcherState::Watching; + *state_guard = WatcherState::Watching; } return Ok(()); } - if *state_guard != WatcherState::Initializing && *state_guard != WatcherState::Stopped && *state_guard != WatcherState::Paused { - return Err(anyhow::anyhow!(format!("Cannot start watcher from state {:?}", *state_guard))); + if *state_guard != WatcherState::Initializing + && *state_guard != WatcherState::Stopped + && *state_guard != WatcherState::Paused + { + return Err(anyhow::anyhow!(format!( + "Cannot start watcher from state {:?}", + *state_guard + ))); } *state_guard = WatcherState::Watching; @@ -526,8 +554,11 @@ impl FileWatcher { *state_guard = WatcherState::Paused; Ok(()) } - WatcherState::Paused => Ok(()), - _ => Err(anyhow::anyhow!(format!("Watcher not in watching state to pause (current: {:?})", *state_guard))), + WatcherState::Paused => Ok(()), + _ => Err(anyhow::anyhow!(format!( + "Watcher not in watching state to pause (current: {:?})", + *state_guard + ))), } } @@ -541,8 +572,11 @@ impl FileWatcher { *state_guard = WatcherState::Watching; Ok(()) } - WatcherState::Watching => Ok(()), - _ => Err(anyhow::anyhow!(format!("Watcher not in paused state to resume (current: {:?})", *state_guard))), + WatcherState::Watching => Ok(()), + _ => Err(anyhow::anyhow!(format!( + "Watcher not in paused state to resume (current: {:?})", + *state_guard + ))), } } @@ -551,7 +585,9 @@ impl FileWatcher { .state .lock() .map_err(|_| anyhow::anyhow!("state mutex poisoned"))?; - if *current_state_guard == WatcherState::Stopped || *current_state_guard == WatcherState::ShuttingDown { + if *current_state_guard == WatcherState::Stopped + || *current_state_guard == WatcherState::ShuttingDown + { return Ok(()); } *current_state_guard = WatcherState::ShuttingDown; @@ -567,7 +603,7 @@ impl FileWatcher { } } } - + let mut final_state_guard = self .state .lock() @@ -600,12 +636,11 @@ impl Drop for FileWatcher { } } - #[cfg(test)] -mod file_watcher_state_tests { +mod file_watcher_state_tests { use super::*; - use tempfile::tempdir; - use std::fs as FsMod; // Alias to avoid conflict with local `fs` module name if any + use std::fs as FsMod; + use tempfile::tempdir; // Alias to avoid conflict with local `fs` module name if any #[test] fn test_watcher_pause_resume_stop() { @@ -615,7 +650,8 @@ mod file_watcher_state_tests { let config = WatcherConfig::default(); - let mut watcher = FileWatcher::new(vec![watch_path], config).expect("Failed to create watcher"); + let mut watcher = + FileWatcher::new(vec![watch_path], config).expect("Failed to create watcher"); assert_eq!(watcher.status().unwrap().state, WatcherState::Initializing); @@ -630,7 +666,7 @@ mod file_watcher_state_tests { watcher.resume().expect("Resume failed"); assert_eq!(watcher.status().unwrap().state, WatcherState::Watching); - + watcher.resume().expect("Second resume failed"); assert_eq!(watcher.status().unwrap().state, WatcherState::Watching); @@ -645,37 +681,43 @@ mod file_watcher_state_tests { fn test_watcher_start_errors() { let tmp_dir = tempdir().unwrap(); FsMod::create_dir_all(tmp_dir.path()).expect("Failed to create temp dir for watching"); - let mut watcher = FileWatcher::new(vec![tmp_dir.path().to_path_buf()], WatcherConfig::default()).unwrap(); - + let mut watcher = + FileWatcher::new(vec![tmp_dir.path().to_path_buf()], WatcherConfig::default()).unwrap(); + { - let mut state_guard = watcher - .state - .lock() - .expect("state mutex poisoned"); + let mut state_guard = watcher.state.lock().expect("state mutex poisoned"); *state_guard = WatcherState::Watching; } - assert!(watcher.start().is_ok(), "Should be able to call start when already Watching (idempotent state change)"); + assert!( + watcher.start().is_ok(), + "Should be able to call start when already Watching (idempotent state change)" + ); assert_eq!(watcher.status().unwrap().state, WatcherState::Watching); - + { - let mut state_guard = watcher - .state - .lock() - .expect("state mutex poisoned"); + let mut state_guard = watcher.state.lock().expect("state mutex poisoned"); *state_guard = WatcherState::ShuttingDown; } - assert!(watcher.start().is_err(), "Should not be able to start from ShuttingDown"); + assert!( + watcher.start().is_err(), + "Should not be able to start from ShuttingDown" + ); } - #[test] + #[test] fn test_new_watcher_with_nonexistent_path() { - let non_existent_path = PathBuf::from("/path/that/REALLY/does/not/exist/for/sure/and/cannot/be/created"); + let non_existent_path = + PathBuf::from("/path/that/REALLY/does/not/exist/for/sure/and/cannot/be/created"); let config = WatcherConfig::default(); let watcher_result = FileWatcher::new(vec![non_existent_path], config); assert!(watcher_result.is_err()); if let Err(e) = watcher_result { let err_string = e.to_string(); - assert!(err_string.contains("Failed to watch path") || err_string.contains("os error 2"), "Error was: {}", err_string); + assert!( + err_string.contains("Failed to watch path") || err_string.contains("os error 2"), + "Error was: {}", + err_string + ); } } @@ -696,7 +738,8 @@ mod file_watcher_state_tests { let config = WatcherConfig::default(); - let mut watcher = FileWatcher::new(vec![watch_path], config).expect("Failed to create watcher"); + let mut watcher = + FileWatcher::new(vec![watch_path], config).expect("Failed to create watcher"); let state_arc = watcher.state.clone(); let _ = std::thread::spawn(move || { diff --git a/libmarlin/src/watcher_tests.rs b/libmarlin/src/watcher_tests.rs index edd6271..53ceb82 100644 --- a/libmarlin/src/watcher_tests.rs +++ b/libmarlin/src/watcher_tests.rs @@ -5,9 +5,8 @@ mod tests { // Updated import for BackupManager from the new backup module use crate::backup::BackupManager; // These are still from the watcher module - use crate::watcher::{FileWatcher, WatcherConfig, WatcherState}; - use crate::db::open as open_marlin_db; // Use your project's DB open function - + use crate::db::open as open_marlin_db; + use crate::watcher::{FileWatcher, WatcherConfig, WatcherState}; // Use your project's DB open function use std::fs::{self, File}; use std::io::Write; @@ -54,7 +53,8 @@ mod tests { .append(true) .open(&test_file_path) .expect("Failed to open test file for modification"); - writeln!(existing_file_handle, "Additional content").expect("Failed to append to test file"); + writeln!(existing_file_handle, "Additional content") + .expect("Failed to append to test file"); drop(existing_file_handle); thread::sleep(Duration::from_millis(200)); @@ -64,49 +64,84 @@ mod tests { watcher.stop().expect("Failed to stop watcher"); assert_eq!(watcher.status().unwrap().state, WatcherState::Stopped); - assert!(watcher.status().unwrap().events_processed > 0, "Expected some file events to be processed"); + assert!( + watcher.status().unwrap().events_processed > 0, + "Expected some file events to be processed" + ); } #[test] fn test_backup_manager_related_functionality() { let live_db_tmp_dir = tempdir().expect("Failed to create temp directory for live DB"); - let backups_storage_tmp_dir = tempdir().expect("Failed to create temp directory for backups storage"); - + let backups_storage_tmp_dir = + tempdir().expect("Failed to create temp directory for backups storage"); + let live_db_path = live_db_tmp_dir.path().join("test_live_watcher.db"); // Unique name let backups_actual_dir = backups_storage_tmp_dir.path().join("my_backups_watcher"); // Unique name // Initialize a proper SQLite DB for the "live" database - let _conn = open_marlin_db(&live_db_path).expect("Failed to open test_live_watcher.db for backup test"); - + let _conn = open_marlin_db(&live_db_path) + .expect("Failed to open test_live_watcher.db for backup test"); + let backup_manager = BackupManager::new(&live_db_path, &backups_actual_dir) .expect("Failed to create BackupManager instance"); - - let backup_info = backup_manager.create_backup().expect("Failed to create first backup"); - - assert!(backups_actual_dir.join(&backup_info.id).exists(), "Backup file should exist"); - assert!(backup_info.size_bytes > 0, "Backup size should be greater than 0"); - + + let backup_info = backup_manager + .create_backup() + .expect("Failed to create first backup"); + + assert!( + backups_actual_dir.join(&backup_info.id).exists(), + "Backup file should exist" + ); + assert!( + backup_info.size_bytes > 0, + "Backup size should be greater than 0" + ); + for i in 0..3 { std::thread::sleep(std::time::Duration::from_millis(30)); // Ensure timestamp difference - backup_manager.create_backup().unwrap_or_else(|e| panic!("Failed to create additional backup {}: {:?}", i, e)); + backup_manager + .create_backup() + .unwrap_or_else(|e| panic!("Failed to create additional backup {}: {:?}", i, e)); } - - let backups = backup_manager.list_backups().expect("Failed to list backups"); + + let backups = backup_manager + .list_backups() + .expect("Failed to list backups"); assert_eq!(backups.len(), 4, "Should have 4 backups listed"); - + let prune_result = backup_manager.prune(2).expect("Failed to prune backups"); - + assert_eq!(prune_result.kept.len(), 2, "Should have kept 2 backups"); - assert_eq!(prune_result.removed.len(), 2, "Should have removed 2 backups (4 initial - 2 kept)"); - - let remaining_backups = backup_manager.list_backups().expect("Failed to list backups after prune"); - assert_eq!(remaining_backups.len(), 2, "Should have 2 backups remaining after prune"); + assert_eq!( + prune_result.removed.len(), + 2, + "Should have removed 2 backups (4 initial - 2 kept)" + ); + + let remaining_backups = backup_manager + .list_backups() + .expect("Failed to list backups after prune"); + assert_eq!( + remaining_backups.len(), + 2, + "Should have 2 backups remaining after prune" + ); for removed_info in prune_result.removed { - assert!(!backups_actual_dir.join(&removed_info.id).exists(), "Removed backup file {} should not exist", removed_info.id); + assert!( + !backups_actual_dir.join(&removed_info.id).exists(), + "Removed backup file {} should not exist", + removed_info.id + ); } for kept_info in prune_result.kept { - assert!(backups_actual_dir.join(&kept_info.id).exists(), "Kept backup file {} should exist", kept_info.id); + assert!( + backups_actual_dir.join(&kept_info.id).exists(), + "Kept backup file {} should exist", + kept_info.id + ); } } }