From 4534ab49cb12d7623f0722cfb3a6a8c6d90355c2 Mon Sep 17 00:00:00 2001 From: StudentWeis Date: Sat, 25 Apr 2026 18:14:37 +0800 Subject: [PATCH] refactor(core): decouple progress reporting from indicatif MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a `ProgressReporter` trait in `core::progress` with a `NoopProgress` zero-cost default implementation. The `core` layer now only talks to `&dyn ProgressReporter` and no longer depends on `indicatif` in any of its public signatures — keeping terminal-rendering concerns out of the reusable library surface. Breaking API changes (pre-1.0, acceptable): - Remove `find_references_with_progress`, `mv_with_progress`, `rename_with_progress`, and `preview_move_with_progress`. - Merge the progress parameter directly into `find_references`, `mv`, `preview_move`, and `rename`; callers that don't care pass `&NoopProgress`. - `src/lib.rs` re-exports now include `ProgressReporter` and `NoopProgress`, and drop the removed `*_with_progress` variants. CLI adapter: - `src/commands/progress.rs` replaces `create_spinner`/`finish` with a `Spinner` newtype that implements `ProgressReporter`, bridging indicatif to the core trait without violating the orphan rule. Refs #4 --- benches/benchmark.rs | 12 ++-- benches/support/mod.rs | 3 +- src/commands/find.rs | 10 +-- src/commands/mv.rs | 18 ++--- src/commands/progress.rs | 74 +++++++++++++++++---- src/commands/rename.rs | 18 ++--- src/core/find.rs | 42 ++++++------ src/core/mod.rs | 1 + src/core/mv.rs | 107 ++++++++++++------------------ src/core/progress.rs | 107 ++++++++++++++++++++++++++++++ src/core/rename.rs | 68 ++++++++++--------- src/lib.rs | 7 +- tests/bench_fixture_tests.rs | 10 +-- tests/lib_find_tests.rs | 28 ++++---- tests/lib_mv_tests.rs | 125 ++++++++++++++++++++++++++++++++--- tests/lib_rename_tests.rs | 51 +++++++++++--- 16 files changed, 479 insertions(+), 202 deletions(-) create mode 100644 src/core/progress.rs diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 018010a..726b8dd 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -1,7 +1,7 @@ use std::hint::black_box; use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; -use mdref::{find_links, find_references}; +use mdref::{NoopProgress, find_links, find_references}; mod support; @@ -43,9 +43,12 @@ fn benchmark_find_operations(c: &mut Criterion) { &fixture, |b, fixture| { b.iter(|| { - let result = - find_references(black_box(&fixture.hot_file), black_box(&fixture.root)) - .expect("find_references benchmark should succeed"); + let result = find_references( + black_box(&fixture.hot_file), + black_box(&fixture.root), + &NoopProgress, + ) + .expect("find_references benchmark should succeed"); black_box(result); }); }, @@ -60,6 +63,7 @@ fn benchmark_find_operations(c: &mut Criterion) { let result = find_references( black_box(&fixture.hot_directory), black_box(&fixture.root), + &NoopProgress, ) .expect("find_references directory benchmark should succeed"); black_box(result); diff --git a/benches/support/mod.rs b/benches/support/mod.rs index 603393f..c98e928 100644 --- a/benches/support/mod.rs +++ b/benches/support/mod.rs @@ -5,7 +5,7 @@ use std::{ path::{Path, PathBuf}, }; -use mdref::{Result, diff_paths, mv}; +use mdref::{NoopProgress, Result, diff_paths, mv}; use tempfile::TempDir; const FIXED_MARKDOWN_FILES: usize = 4; @@ -121,6 +121,7 @@ pub fn run_move_operation(operation: MoveOperation<'_>) -> Result<()> { black_box(operation.destination), black_box(operation.root), false, + &NoopProgress, ) } diff --git a/src/commands/find.rs b/src/commands/find.rs index 5b5d805..9258fcd 100644 --- a/src/commands/find.rs +++ b/src/commands/find.rs @@ -1,9 +1,9 @@ use std::io::Write; -use mdref::{MdrefError, Reference, Result, find_links, find_references_with_progress}; +use mdref::{MdrefError, Reference, Result, find_links, find_references}; use serde::Serialize; -use super::{OutputFormat, progress}; +use super::{OutputFormat, progress::Spinner}; pub fn run( path: String, @@ -25,11 +25,11 @@ fn run_with_writer( let root_path = root_dir.unwrap_or_else(|| ".".to_string()); // Find references to the specified file. - let progress = progress::create_spinner(show_progress); + let spinner = Spinner::new(show_progress); - let references = find_references_with_progress(&path, &root_path, progress.as_ref())?; + let references = find_references(&path, &root_path, spinner.as_reporter())?; - progress::finish(&progress); + spinner.finish(); // Find all links within the specified file. let links = find_links(&path)?; diff --git a/src/commands/mv.rs b/src/commands/mv.rs index bfe3c4b..3964620 100644 --- a/src/commands/mv.rs +++ b/src/commands/mv.rs @@ -1,10 +1,10 @@ use std::io::Write; -use mdref::{Result, core::mv::preview_move_with_progress, mv_with_progress}; +use mdref::{NoopProgress, Result, mv, preview_move}; use serde::Serialize; use crate::commands::{ - OutputFormat, json_move_changes, progress, write_json_output, write_move_preview_human, + OutputFormat, json_move_changes, progress::Spinner, write_json_output, write_move_preview_human, }; pub fn run( @@ -38,30 +38,30 @@ fn run_with_writer( ) -> Result<()> { let root = root.unwrap_or_else(|| ".".to_string()); - let progress = progress::create_spinner(show_progress && !dry_run); + let spinner = Spinner::new(show_progress && !dry_run); match format { OutputFormat::Human => { if dry_run { - let preview = preview_move_with_progress(&source, &dest, &root, None)?; + let preview = preview_move(&source, &dest, &root, &NoopProgress)?; return write_move_preview_human(&preview, writer); } writeln!(writer, "Move {source} -> {dest} in {root}")?; - let result = mv_with_progress(&source, &dest, &root, false, progress.as_ref()); + let result = mv(&source, &dest, &root, false, spinner.as_reporter()); - progress::finish(&progress); + spinner.finish(); result } OutputFormat::Json => { - let preview = preview_move_with_progress(&source, &dest, &root, None)?; + let preview = preview_move(&source, &dest, &root, &NoopProgress)?; if !dry_run { - mv_with_progress(&source, &dest, &root, false, progress.as_ref())?; + mv(&source, &dest, &root, false, spinner.as_reporter())?; } - progress::finish(&progress); + spinner.finish(); let payload = MoveCommandOutput { operation: "mv", diff --git a/src/commands/progress.rs b/src/commands/progress.rs index 0f69d5d..d355502 100644 --- a/src/commands/progress.rs +++ b/src/commands/progress.rs @@ -1,25 +1,71 @@ use indicatif::{ProgressBar, ProgressStyle}; +use mdref::ProgressReporter; const SPINNER_TEMPLATE: &str = "{spinner:.green} [{pos}/{len}] {msg}"; -/// Create an optional spinner progress bar. +/// A CLI-side progress handle that renders an `indicatif` spinner and exposes +/// itself as a generic [`ProgressReporter`] to the `core` layer. /// -/// Returns `Some(ProgressBar)` when `enabled` is `true`, `None` otherwise. -/// All command modules share the same spinner style, so this centralises the -/// template in one place. -pub fn create_spinner(enabled: bool) -> Option { - if !enabled { - return None; +/// The type doubles as a newtype adapter: `core` never sees `indicatif` at +/// all, and Rust's orphan rule is satisfied because `Spinner` is defined in +/// this crate. When `enabled` is `false`, the spinner is absent and all +/// reporter methods become no-ops. +/// +/// Use [`Spinner::as_reporter`] when calling into `core`, and drop the +/// [`Spinner`] (or call [`Spinner::finish`]) to clear the terminal line when +/// done. +pub struct Spinner { + bar: Option, +} + +impl Spinner { + /// Create a spinner that renders when `enabled` is `true`, or a silent + /// no-op reporter otherwise. Both variants share the same `ProgressReporter` + /// contract, so the caller code stays branch-free. + pub fn new(enabled: bool) -> Self { + if !enabled { + return Self { bar: None }; + } + + let progress_bar = ProgressBar::new_spinner(); + progress_bar + .set_style(ProgressStyle::with_template(SPINNER_TEMPLATE).expect("valid template")); + Self { + bar: Some(progress_bar), + } + } + + /// Borrow this spinner as a `&dyn ProgressReporter` for `core` APIs. + pub fn as_reporter(&self) -> &dyn ProgressReporter { + self } - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_style(ProgressStyle::with_template(SPINNER_TEMPLATE).expect("valid template")); - Some(progress_bar) + /// Finish and clear the underlying spinner, if any. + pub fn finish(&self) { + if let Some(bar) = &self.bar { + bar.finish_and_clear(); + } + } } -/// Finish and clear the progress bar, if present. -pub fn finish(progress: &Option) { - if let Some(progress_bar) = progress { - progress_bar.finish_and_clear(); +impl ProgressReporter for Spinner { + fn set_message(&self, message: &str) { + if let Some(bar) = &self.bar { + // indicatif requires an owned `Cow<'static, str>` for messages, so + // we allocate here. This only runs a handful of times per command. + bar.set_message(message.to_owned()); + } + } + + fn set_total(&self, total: u64) { + if let Some(bar) = &self.bar { + bar.set_length(total); + } + } + + fn inc(&self, delta: u64) { + if let Some(bar) = &self.bar { + bar.inc(delta); + } } } diff --git a/src/commands/rename.rs b/src/commands/rename.rs index 46e047d..ab1fd2c 100644 --- a/src/commands/rename.rs +++ b/src/commands/rename.rs @@ -1,10 +1,10 @@ use std::io::Write; -use mdref::{Result, core::mv::preview_move_with_progress, rename_with_progress}; +use mdref::{NoopProgress, Result, preview_move, rename}; use serde::Serialize; use crate::commands::{ - OutputFormat, json_move_changes, progress, write_json_output, write_move_preview_human, + OutputFormat, json_move_changes, progress::Spinner, write_json_output, write_move_preview_human, }; pub fn run( @@ -31,30 +31,30 @@ fn run_with_writer( let root_path = root.unwrap_or_else(|| ".".to_string()); let destination = std::path::Path::new(&old).with_file_name(&new); - let progress = progress::create_spinner(show_progress && !dry_run); + let spinner = Spinner::new(show_progress && !dry_run); match format { OutputFormat::Human => { if dry_run { - let preview = preview_move_with_progress(&old, &destination, &root_path, None)?; + let preview = preview_move(&old, &destination, &root_path, &NoopProgress)?; return write_move_preview_human(&preview, writer); } writeln!(writer, "Rename {old} -> {new} in {root_path}")?; - let result = rename_with_progress(&old, &new, &root_path, false, progress.as_ref()); + let result = rename(&old, &new, &root_path, false, spinner.as_reporter()); - progress::finish(&progress); + spinner.finish(); result } OutputFormat::Json => { - let preview = preview_move_with_progress(&old, &destination, &root_path, None)?; + let preview = preview_move(&old, &destination, &root_path, &NoopProgress)?; if !dry_run { - rename_with_progress(&old, &new, &root_path, false, progress.as_ref())?; + rename(&old, &new, &root_path, false, spinner.as_reporter())?; } - progress::finish(&progress); + spinner.finish(); let payload = RenameCommandOutput { operation: "rename", diff --git a/src/core/find.rs b/src/core/find.rs index 1f7c324..a7107a9 100644 --- a/src/core/find.rs +++ b/src/core/find.rs @@ -9,32 +9,32 @@ use comrak::{ nodes::{AstNode, NodeValue}, parse_document, }; -use indicatif::ProgressBar; use rayon::prelude::*; -use super::util::{ - collect_markdown_files, is_external_url, strip_anchor, strip_utf8_bom_prefix, url_decode_link, +use super::{ + progress::ProgressReporter, + util::{ + collect_markdown_files, is_external_url, strip_anchor, strip_utf8_bom_prefix, + url_decode_link, + }, }; use crate::{Reference, Result}; /// Find all references to a given file within Markdown files in the specified root directory. -/// Returns a vector of References containing the referencing file path, line number, column number, and the link text. -pub fn find_references(path: P, root_dir: B) -> Result> -where - P: AsRef, - B: AsRef, -{ - find_references_with_progress(path, root_dir, None) -} - -/// Find all references with an optional progress bar. /// -/// When a `ProgressBar` is provided, it is incremented once for each Markdown file scanned. -/// The caller is responsible for creating and finishing the progress bar. -pub fn find_references_with_progress( +/// Returns a vector of [`Reference`]s containing the referencing file path, line number, +/// column number, and the link text. +/// +/// # Progress +/// +/// Callers report progress through a [`ProgressReporter`] trait object. Pass +/// [`crate::NoopProgress`] (as `&NoopProgress`) when progress updates are not needed. +/// The reporter is called with [`ProgressReporter::set_total`] once before scanning, +/// and with [`ProgressReporter::inc`] once per Markdown file as it is processed. +pub fn find_references( path: P, root_dir: B, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result> where P: AsRef, @@ -49,9 +49,7 @@ where })?; let markdown_files = collect_markdown_files(root_dir.as_ref()); - if let Some(progress_bar) = progress { - progress_bar.set_length(markdown_files.len() as u64); - } + progress.set_total(markdown_files.len() as u64); let results: Vec>> = markdown_files .par_iter() @@ -61,9 +59,7 @@ where source: e, })?; let refs = process_md_file(&content, path, Some(&canonical_path)); - if let Some(progress_bar) = progress { - progress_bar.inc(1); - } + progress.inc(1); Ok(refs) }) .collect(); diff --git a/src/core/mod.rs b/src/core/mod.rs index 656da83..24fc5f8 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -2,5 +2,6 @@ pub mod find; pub mod model; pub mod mv; pub mod pathdiff; +pub mod progress; pub mod rename; pub mod util; diff --git a/src/core/mv.rs b/src/core/mv.rs index ead2cc5..36ba8ff 100644 --- a/src/core/mv.rs +++ b/src/core/mv.rs @@ -4,12 +4,12 @@ use std::{ path::{Path, PathBuf}, }; -use indicatif::ProgressBar; use walkdir::WalkDir; use super::{ - find::find_references_with_progress, + find::find_references, model::{LinkReplacement, MoveChange, MoveChangeKind, MovePreview, MoveTransaction}, + progress::ProgressReporter, util::{ collect_markdown_files, is_external_url, relative_path, strip_utf8_bom_prefix, url_decode_link, @@ -393,7 +393,7 @@ fn plan_directory_replacements( source_canonical: &Path, dest_canonical: &Path, root: &Path, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result<(ReplacementPlan, SnapshotPaths)> { let path_mappings = build_directory_path_mappings(source_dir, source_canonical, dest_canonical)?; @@ -401,10 +401,8 @@ fn plan_directory_replacements( let mut snapshot_paths: HashSet = HashSet::new(); let mut line_cache = LineCache::new(); - if let Some(progress_bar) = progress { - progress_bar.set_message("Scanning references..."); - } - for reference in find_references_with_progress(source_dir, root, progress)? { + progress.set_message("Scanning references..."); + for reference in find_references(source_dir, root, progress)? { let (link_path_only, _) = split_link_and_anchor(&reference.link_text); let Some(old_target) = resolve_reference_target(&reference.path, link_path_only) else { continue; @@ -544,39 +542,18 @@ fn add_destination_replacements( /// /// If the destination path is an existing directory, the source file will be moved into that /// directory with its original filename preserved. -pub fn mv(source: P, dest: B, root: D, dry_run: bool) -> Result<()> -where - P: AsRef, - B: AsRef, - D: AsRef, -{ - mv_with_progress(source, dest, root, dry_run, None) -} - -/// Move a Markdown file or directory and atomically update all references, -/// with an optional progress bar for visual feedback. /// -/// When a `ProgressBar` is provided, it is used to display scanning and update progress. -/// The caller is responsible for creating and finishing the progress bar. -pub fn preview_move(source: P, dest: B, root: D) -> Result -where - P: AsRef, - B: AsRef, - D: AsRef, -{ - preview_move_with_progress(source, dest, root, None) -} - -/// Preview a Markdown move without mutating the filesystem. +/// # Progress /// -/// The returned preview contains the resolved destination path and all link -/// replacements that would be applied by the move. -pub fn preview_move_with_progress( +/// Callers pass a [`ProgressReporter`] trait object to receive scanning progress. +/// Pass [`crate::NoopProgress`] (as `&NoopProgress`) when progress updates are not needed. +pub fn mv( source: P, dest: B, root: D, - progress: Option<&ProgressBar>, -) -> Result + dry_run: bool, + progress: &dyn ProgressReporter, +) -> Result<()> where P: AsRef, B: AsRef, @@ -587,19 +564,27 @@ where let root = root.as_ref(); if source.is_dir() { - return preview_directory_move(source, dest, root, progress); + return mv_directory(source, dest, root, dry_run, progress); } - preview_regular_file_move(source, dest, root, progress) + mv_regular_file(source, dest, root, dry_run, progress) } -pub fn mv_with_progress( +/// Preview a Markdown move without mutating the filesystem. +/// +/// The returned preview contains the resolved destination path and all link +/// replacements that would be applied by the move. +/// +/// # Progress +/// +/// Callers pass a [`ProgressReporter`] trait object to receive scanning progress. +/// Pass [`crate::NoopProgress`] (as `&NoopProgress`) when progress updates are not needed. +pub fn preview_move( source: P, dest: B, root: D, - dry_run: bool, - progress: Option<&ProgressBar>, -) -> Result<()> + progress: &dyn ProgressReporter, +) -> Result where P: AsRef, B: AsRef, @@ -610,17 +595,17 @@ where let root = root.as_ref(); if source.is_dir() { - return mv_directory(source, dest, root, dry_run, progress); + return preview_directory_move(source, dest, root, progress); } - mv_regular_file(source, dest, root, dry_run, progress) + preview_regular_file_move(source, dest, root, progress) } fn preview_regular_file_move( source: &Path, dest: &Path, root: &Path, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result { if let Some(case_only_dest) = resolve_case_only_destination(source, dest)? { return preview_case_only_file_move(source, &case_only_dest, root, progress); @@ -637,10 +622,8 @@ fn preview_regular_file_move( } }; - if let Some(progress_bar) = progress { - progress_bar.set_message("Scanning references..."); - } - let references = find_references_with_progress(source, root, progress)?; + progress.set_message("Scanning references..."); + let references = find_references(source, root, progress)?; let mut replacements_by_file = plan_external_replacements(&references, &resolved_dest)?; replacements_by_file.remove(source); let internal_replacements = plan_internal_replacements(source, source, &resolved_dest)?; @@ -661,12 +644,10 @@ fn preview_case_only_file_move( source: &Path, resolved_dest: &Path, root: &Path, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result { - if let Some(progress_bar) = progress { - progress_bar.set_message("Scanning references..."); - } - let references = find_references_with_progress(source, root, progress)?; + progress.set_message("Scanning references..."); + let references = find_references(source, root, progress)?; let mut replacements_by_file = plan_case_only_external_replacements(&references, resolved_dest)?; move_source_replacements_to_destination(&mut replacements_by_file, source, resolved_dest); @@ -682,7 +663,7 @@ fn preview_directory_move( source_dir: &Path, new_path: &Path, root: &Path, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result { let (resolved_dest, source_canonical, dest_canonical) = match validate_move_paths(source_dir, new_path) { @@ -715,7 +696,7 @@ fn mv_regular_file( dest: &Path, root: &Path, dry_run: bool, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result<()> { if let Some(case_only_dest) = resolve_case_only_destination(source, dest)? { return mv_case_only_file(source, &case_only_dest, root, dry_run, progress); @@ -734,10 +715,8 @@ fn mv_regular_file( }; // Phase 1: Plan — pure computation, no side effects. - if let Some(progress_bar) = progress { - progress_bar.set_message("Scanning references..."); - } - let references = find_references_with_progress(source, root, progress)?; + progress.set_message("Scanning references..."); + let references = find_references(source, root, progress)?; let mut replacements_by_file = plan_external_replacements(&references, &resolved_dest)?; replacements_by_file.remove(source); let internal_replacements = plan_internal_replacements(source, source, &resolved_dest)?; @@ -827,12 +806,10 @@ fn mv_case_only_file( resolved_dest: &Path, root: &Path, dry_run: bool, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result<()> { - if let Some(progress_bar) = progress { - progress_bar.set_message("Scanning references..."); - } - let references = find_references_with_progress(source, root, progress)?; + progress.set_message("Scanning references..."); + let references = find_references(source, root, progress)?; let mut replacements_by_file = plan_case_only_external_replacements(&references, resolved_dest)?; move_source_replacements_to_destination(&mut replacements_by_file, source, resolved_dest); @@ -870,7 +847,7 @@ fn mv_directory( new_path: &Path, root: &Path, dry_run: bool, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result<()> { let (resolved_dest, source_canonical, dest_canonical) = match validate_move_paths(source_dir, new_path) { diff --git a/src/core/progress.rs b/src/core/progress.rs new file mode 100644 index 0000000..2cb320c --- /dev/null +++ b/src/core/progress.rs @@ -0,0 +1,107 @@ +//! Progress reporting abstraction shared by the `core` layer. +//! +//! The `core` layer stays intentionally unaware of how progress is rendered. +//! It only talks to a [`ProgressReporter`] trait, and any UI concern +//! (indicatif spinners, log lines, test counters, GUI indicators, …) lives +//! entirely in the consumer. +//! +//! This keeps `core` free of any terminal-rendering dependency such as +//! `indicatif`, and makes the library usable from non-CLI contexts (LSP, MCP +//! servers, GUIs) without forcing a UI crate onto them. + +/// A sink for progress signals emitted by long-running `core` operations. +/// +/// All methods have a no-op default body so that consumers can implement only +/// the signals they care about. Use [`NoopProgress`] when you want to silence +/// progress entirely. +/// +/// The `Sync` super-trait bound lets a `&dyn ProgressReporter` be shared +/// across `rayon` worker threads (see [`crate::find_references`]). +pub trait ProgressReporter: Sync { + /// Attach a short, human-readable phase label (e.g. `"Scanning references..."`). + fn set_message(&self, _message: &str) {} + + /// Declare the total amount of work the current phase is about to do. + fn set_total(&self, _total: u64) {} + + /// Report that `delta` additional units of work have been completed. + fn inc(&self, _delta: u64) {} +} + +/// A zero-cost no-op reporter used when the caller doesn't care about progress. +/// +/// Pass `&NoopProgress` to any `core` function that takes `&dyn ProgressReporter`. +#[derive(Debug, Default, Clone, Copy)] +pub struct NoopProgress; + +impl ProgressReporter for NoopProgress {} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicU64, Ordering}; + + use super::*; + + #[test] + fn test_noop_progress_does_nothing() { + // Should not panic and should not require any side effect. + let reporter = NoopProgress; + reporter.set_message("hello"); + reporter.set_total(42); + reporter.inc(1); + } + + #[test] + fn test_noop_progress_works_behind_trait_object() { + // Guards the intended usage: callers pass `&NoopProgress` as + // `&dyn ProgressReporter` into `core` functions. + let reporter: &dyn ProgressReporter = &NoopProgress; + reporter.set_message("phase"); + reporter.set_total(10); + reporter.inc(3); + } + + /// A counting reporter used to verify that custom implementations + /// receive the events they care about. + #[derive(Default)] + struct CountingReporter { + total: AtomicU64, + ticks: AtomicU64, + } + + impl ProgressReporter for CountingReporter { + fn set_total(&self, total: u64) { + self.total.store(total, Ordering::SeqCst); + } + + fn inc(&self, delta: u64) { + self.ticks.fetch_add(delta, Ordering::SeqCst); + } + } + + #[test] + fn test_custom_reporter_receives_set_total_and_inc() { + let reporter = CountingReporter::default(); + + let dyn_reporter: &dyn ProgressReporter = &reporter; + dyn_reporter.set_total(5); + dyn_reporter.inc(2); + dyn_reporter.inc(3); + + assert_eq!(reporter.total.load(Ordering::SeqCst), 5); + assert_eq!(reporter.ticks.load(Ordering::SeqCst), 5); + } + + #[test] + fn test_custom_reporter_unimplemented_methods_are_noop() { + // A reporter may legitimately skip `set_message`; the default body + // guarantees that call sites don't have to special-case it. + let reporter = CountingReporter::default(); + let dyn_reporter: &dyn ProgressReporter = &reporter; + + dyn_reporter.set_message("ignored"); + + assert_eq!(reporter.total.load(Ordering::SeqCst), 0); + assert_eq!(reporter.ticks.load(Ordering::SeqCst), 0); + } +} diff --git a/src/core/rename.rs b/src/core/rename.rs index 51c5c78..b845d38 100644 --- a/src/core/rename.rs +++ b/src/core/rename.rs @@ -1,11 +1,9 @@ use std::path::Path; -use indicatif::ProgressBar; - -use crate::{Result, core::mv::mv_with_progress, mv}; +use crate::{Result, core::progress::ProgressReporter, mv}; /// Rename a file by changing only its filename while keeping it in the same directory. -/// This is a convenience wrapper around `mv` that handles the common case of +/// This is a convenience wrapper around [`mv`] that handles the common case of /// renaming a file in place. /// /// # Arguments @@ -14,6 +12,7 @@ use crate::{Result, core::mv::mv_with_progress, mv}; /// * `name` - The new filename (not a path, just the filename) /// * `root` - Root directory to search for references /// * `dry_run` - If true, only preview changes without making them +/// * `progress` - Progress reporter; pass `&NoopProgress` when not needed /// /// # Returns /// @@ -22,35 +21,17 @@ use crate::{Result, core::mv::mv_with_progress, mv}; /// # Example /// /// ```ignore -/// use mdref::rename; +/// use mdref::{rename, NoopProgress}; /// /// // Rename "old.md" to "new.md" in the same directory -/// rename("docs/old.md", "new.md", ".", false)?; +/// rename("docs/old.md", "new.md", ".", false, &NoopProgress)?; /// ``` -pub fn rename(source: P, name: B, root: D, dry_run: bool) -> Result<()> -where - P: AsRef, - B: AsRef, - D: AsRef, -{ - let source = source.as_ref(); - let name = name.as_ref(); - let root = root.as_ref(); - - let new_path = source.with_file_name(name); - - mv(source, new_path, root, dry_run) -} - -/// Rename a file with an optional progress bar for visual feedback. -/// -/// This is a convenience wrapper around `mv_with_progress`. -pub fn rename_with_progress( +pub fn rename( source: P, name: B, root: D, dry_run: bool, - progress: Option<&ProgressBar>, + progress: &dyn ProgressReporter, ) -> Result<()> where P: AsRef, @@ -63,7 +44,7 @@ where let new_path = source.with_file_name(name); - mv_with_progress(source, new_path, root, dry_run, progress) + mv(source, new_path, root, dry_run, progress) } #[cfg(test)] @@ -73,7 +54,7 @@ mod tests { use tempfile::TempDir; use super::*; - use crate::{MdrefError, test_utils::write_file}; + use crate::{MdrefError, core::progress::NoopProgress, test_utils::write_file}; #[test] #[allow(clippy::unwrap_used)] @@ -82,7 +63,14 @@ mod tests { let source = temp_dir.path().join("guide.md"); write_file(&source, "# Guide"); - rename(&source, "guide-v2.md", temp_dir.path(), false).unwrap(); + rename( + &source, + "guide-v2.md", + temp_dir.path(), + false, + &NoopProgress, + ) + .unwrap(); assert!(!source.exists()); assert!(temp_dir.path().join("guide-v2.md").exists()); @@ -97,7 +85,14 @@ mod tests { write_file(&source, "# Topic"); write_file(&index, "See [topic](topic.md)."); - rename(&source, "topic-v2.md", temp_dir.path(), false).unwrap(); + rename( + &source, + "topic-v2.md", + temp_dir.path(), + false, + &NoopProgress, + ) + .unwrap(); let index_content = fs::read_to_string(&index).unwrap(); assert!(index_content.contains("topic-v2.md")); @@ -111,7 +106,7 @@ mod tests { let source = temp_dir.path().join("page.md"); write_file(&source, "[Self](page.md)\n[Section](#intro)"); - rename(&source, "page-v2.md", temp_dir.path(), false).unwrap(); + rename(&source, "page-v2.md", temp_dir.path(), false, &NoopProgress).unwrap(); let renamed_content = fs::read_to_string(temp_dir.path().join("page-v2.md")).unwrap(); assert!(renamed_content.contains("[Self](page-v2.md)")); @@ -128,7 +123,14 @@ mod tests { write_file(&source, "# Draft\n\n[Self](draft.md)"); write_file(&index, "[Draft](draft.md)"); - rename(&source, "published.md", temp_dir.path(), true).unwrap(); + rename( + &source, + "published.md", + temp_dir.path(), + true, + &NoopProgress, + ) + .unwrap(); assert!(source.exists()); assert!(!temp_dir.path().join("published.md").exists()); @@ -148,7 +150,7 @@ mod tests { write_file(&source, "# Source"); write_file(&existing_target, "# Existing"); - let error = rename(&source, "taken.md", temp_dir.path(), false).unwrap_err(); + let error = rename(&source, "taken.md", temp_dir.path(), false, &NoopProgress).unwrap_err(); assert!(matches!(error, MdrefError::PathValidation { .. })); assert!( diff --git a/src/lib.rs b/src/lib.rs index 837f683..17faa3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,12 @@ mod error; pub mod test_utils; pub use core::{ - find::{find_links, find_references, find_references_with_progress}, + find::{find_links, find_references}, model::{LinkType, Reference}, - mv::{mv, mv_with_progress}, + mv::{mv, preview_move}, pathdiff::diff_paths, - rename::{rename, rename_with_progress}, + progress::{NoopProgress, ProgressReporter}, + rename::rename, }; pub use error::{MdrefError, Result}; diff --git a/tests/bench_fixture_tests.rs b/tests/bench_fixture_tests.rs index 2fa3b66..9398287 100644 --- a/tests/bench_fixture_tests.rs +++ b/tests/bench_fixture_tests.rs @@ -4,7 +4,7 @@ mod support; use std::path::Path; -use mdref::{LinkType, find_links, find_references}; +use mdref::{LinkType, NoopProgress, find_links, find_references}; use rstest::rstest; use support::{ BenchmarkFixture, FixtureProfile, FixtureSummary, MoveOperation, build_fixture, @@ -33,8 +33,10 @@ fn test_small_profile_reports_expected_summary() { fn test_fixture_reference_counts_match_summary() { let fixture = build_fixture(FixtureProfile::Small).unwrap(); - let hot_file_references = find_references(&fixture.hot_file, &fixture.root).unwrap(); - let bundle_references = find_references(&fixture.hot_directory, &fixture.root).unwrap(); + let hot_file_references = + find_references(&fixture.hot_file, &fixture.root, &NoopProgress).unwrap(); + let bundle_references = + find_references(&fixture.hot_directory, &fixture.root, &NoopProgress).unwrap(); assert_eq!( hot_file_references.len(), @@ -187,7 +189,7 @@ fn test_move_operation_execution_updates_paths_and_references( assert!(!select_source(&fixture).exists()); assert!(select_destination(&fixture).exists()); assert_eq!( - find_references(select_destination(&fixture), &fixture.root) + find_references(select_destination(&fixture), &fixture.root, &NoopProgress) .unwrap() .len(), expected_references diff --git a/tests/lib_find_tests.rs b/tests/lib_find_tests.rs index e9ab980..beaa674 100644 --- a/tests/lib_find_tests.rs +++ b/tests/lib_find_tests.rs @@ -1,6 +1,6 @@ use std::{fs, io::Write, path::Path}; -use mdref::{MdrefError, Reference, find_links, find_references}; +use mdref::{MdrefError, NoopProgress, Reference, find_links, find_references}; use rstest::rstest; use tempfile::TempDir; @@ -270,7 +270,7 @@ fn test_find_links_preserves_dot_slash_prefix() { fn test_find_references_finds_referencing_files() { let fixture = fixture_multi_file_reference(); - let result = find_references(&fixture.target, &fixture.root).unwrap(); + let result = find_references(&fixture.target, &fixture.root, &NoopProgress).unwrap(); assert_eq!(result.len(), 3, "Should find 3 files referencing target.md"); } @@ -278,7 +278,11 @@ fn test_find_references_finds_referencing_files() { #[test] fn test_find_references_returns_error_for_nonexistent_file() { let temp_dir = TempDir::new().unwrap(); - let result = find_references(temp_dir.path().join("ghost.md"), temp_dir.path()); + let result = find_references( + temp_dir.path().join("ghost.md"), + temp_dir.path(), + &NoopProgress, + ); match result { Err(MdrefError::IoRead { path, source }) => { @@ -301,7 +305,7 @@ fn test_find_references_invalid_utf8_file_returns_invalid_data_error() { let invalid_ref = temp_dir.path().join("invalid.md"); fs::write(&invalid_ref, b"[Broken](target.md)\xFF").unwrap(); - let result = find_references(&target, temp_dir.path()); + let result = find_references(&target, temp_dir.path(), &NoopProgress); match result { Err(MdrefError::IoRead { path, source }) => { @@ -324,7 +328,7 @@ fn test_find_references_returns_empty_when_no_references() { let other = temp_dir.path().join("other.md"); write_file(&other, "# Other\n\nNo references here"); - let result = find_references(&target, temp_dir.path()).unwrap(); + let result = find_references(&target, temp_dir.path(), &NoopProgress).unwrap(); assert_eq!( result.len(), 0, @@ -348,7 +352,7 @@ fn test_find_references_only_returns_markdown_files() { let txt_ref = temp_dir.path().join("ref.txt"); write_file(&txt_ref, "See target.md"); - let result = find_references(&target, temp_dir.path()).unwrap(); + let result = find_references(&target, temp_dir.path(), &NoopProgress).unwrap(); for reference in &result { assert_eq!( @@ -378,7 +382,7 @@ fn test_find_references_finds_nested_references() { let sibling_ref = temp_dir.path().join("other.md"); write_file(&sibling_ref, "Check [docs](docs/target.md)"); - let result = find_references(&target, temp_dir.path()).unwrap(); + let result = find_references(&target, temp_dir.path(), &NoopProgress).unwrap(); assert!( result.len() >= 2, "Should find references from different directory levels" @@ -402,7 +406,7 @@ fn test_find_references_respects_gitignore() { let ignored_ref = temp_dir.path().join("ignored").join("ref.md"); write_file(&ignored_ref, "[Ignored](../target.md)"); - let result = find_references(&target, temp_dir.path()).unwrap(); + let result = find_references(&target, temp_dir.path(), &NoopProgress).unwrap(); assert_eq!(result.len(), 1, "Ignored markdown files should be skipped"); assert_eq!(result[0].path, visible_ref); @@ -425,7 +429,7 @@ fn test_find_references_handles_directory_target() { let ref_file = temp_dir.path().join("index.md"); write_file(&ref_file, "See [file1](docs/file1.md)"); - let result = find_references(&dir, temp_dir.path()).unwrap(); + let result = find_references(&dir, temp_dir.path(), &NoopProgress).unwrap(); assert!( !result.is_empty(), "Should find references to files in the directory" @@ -443,7 +447,7 @@ fn test_find_references_detects_self_reference() { let target = temp_dir.path().join("self.md"); write_file(&target, "# Self\n\n[Self link](self.md)"); - let result = find_references(&target, temp_dir.path()).unwrap(); + let result = find_references(&target, temp_dir.path(), &NoopProgress).unwrap(); let self_refs: Vec<&Reference> = result .iter() @@ -470,7 +474,7 @@ fn test_find_references_ignores_external_urls() { "[External](https://example.com/target.md)\n[Local](target.md)", ); - let result = find_references(&target, temp_dir.path()).unwrap(); + let result = find_references(&target, temp_dir.path(), &NoopProgress).unwrap(); // Only the local reference should match assert_eq!( @@ -555,7 +559,7 @@ fn test_find_references_handles_unicode_paths( let ref_file = fixture.root.join(reference_name); write_file(&ref_file, reference_content); - let result = find_references(&target, &fixture.root).unwrap(); + let result = find_references(&target, &fixture.root, &NoopProgress).unwrap(); assert_eq!(result.len(), 1, "Should find one Unicode reference"); assert_eq!(result[0].link_text, expected_link_text); } diff --git a/tests/lib_mv_tests.rs b/tests/lib_mv_tests.rs index 96ad07a..576519e 100644 --- a/tests/lib_mv_tests.rs +++ b/tests/lib_mv_tests.rs @@ -6,7 +6,7 @@ use std::{ sync::{LazyLock, Mutex}, }; -use mdref::{MdrefError, find_links, find_references, mv}; +use mdref::{MdrefError, NoopProgress, find_links, find_references, mv}; use rstest::rstest; use tempfile::TempDir; @@ -60,6 +60,7 @@ fn test_mv_same_directory_moves_file_to_target_path() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Verify @@ -87,6 +88,7 @@ fn test_mv_with_references() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Verify @@ -117,6 +119,7 @@ fn test_mv_to_subdirectory() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Verify @@ -145,6 +148,7 @@ fn test_mv_with_internal_links() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Verify @@ -168,6 +172,7 @@ fn test_mv_nonexistent_source() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); match result { @@ -201,6 +206,7 @@ fn test_mv_creates_parent_directory() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Should successfully create directory and move file @@ -224,6 +230,7 @@ fn test_mv_preserves_content() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -257,6 +264,7 @@ fn test_mv_multiple_references() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -290,6 +298,7 @@ fn test_mv_same_name_different_directory() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -312,7 +321,7 @@ fn test_mv_integration_with_find() { write_file(&ref_file, "[Source](source.md)"); // Find references before move - let refs_before = find_references(&source_file, temp_dir.path()).unwrap(); + let refs_before = find_references(&source_file, temp_dir.path(), &NoopProgress).unwrap(); assert!(!refs_before.is_empty()); // Move file @@ -322,6 +331,7 @@ fn test_mv_integration_with_find() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -363,6 +373,7 @@ fn test_mv_deep_nested_move() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -399,6 +410,7 @@ fn test_mv_same_file_multiple_lines_referencing() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -423,6 +435,7 @@ fn test_mv_self_reference_update() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -449,6 +462,7 @@ fn test_mv_same_directory() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -479,6 +493,7 @@ fn test_mv_with_image_links() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -508,6 +523,7 @@ fn test_mv_preserves_external_urls() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -538,6 +554,7 @@ fn test_mv_from_subdir_to_root() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -555,7 +572,14 @@ fn test_mv_directory_updates_external_references() { let fixture = fixture_directory_move(); let target_parent = fixture.root.join("archive"); - mv(&fixture.source_dir, &target_parent, &fixture.root, false).unwrap(); + mv( + &fixture.source_dir, + &target_parent, + &fixture.root, + false, + &NoopProgress, + ) + .unwrap(); assert!(target_parent.join("guide.md").exists()); assert!(target_parent.join("nested").join("topic.md").exists()); @@ -576,6 +600,7 @@ fn test_mv_directory_updates_internal_links_to_outside_files() { &fixture.destination_dir, &fixture.root, false, + &NoopProgress, ) .unwrap(); @@ -591,6 +616,7 @@ fn test_mv_directory_preserves_internal_links_within_directory() { &fixture.destination_dir, &fixture.root, false, + &NoopProgress, ) .unwrap(); @@ -616,7 +642,14 @@ fn test_mv_directory_skips_gitignored_markdown_rewrites() { write_file(&outside_file, "# FAQ"); let destination = temp_dir.path().join("archive").join("docs"); - mv(&source_dir, &destination, temp_dir.path(), false).unwrap(); + mv( + &source_dir, + &destination, + temp_dir.path(), + false, + &NoopProgress, + ) + .unwrap(); let ignored_content = fs::read_to_string(destination.join("ignored").join("secret.md")).unwrap(); @@ -633,7 +666,14 @@ fn test_mv_directory_into_existing_directory_preserves_source_name() { let destination_parent = temp_dir.path().join("archive"); fs::create_dir_all(&destination_parent).unwrap(); - mv(&source_dir, &destination_parent, temp_dir.path(), false).unwrap(); + mv( + &source_dir, + &destination_parent, + temp_dir.path(), + false, + &NoopProgress, + ) + .unwrap(); assert!(destination_parent.join("docs").join("guide.md").exists()); assert!(!source_dir.exists()); @@ -647,7 +687,13 @@ fn test_mv_directory_rejects_moving_into_own_subdirectory() { write_file(source_dir.join("guide.md"), "# Guide"); let invalid_target = source_dir.join("nested").join("archive"); - let result = mv(&source_dir, &invalid_target, temp_dir.path(), false); + let result = mv( + &source_dir, + &invalid_target, + temp_dir.path(), + false, + &NoopProgress, + ); assert!(result.is_err()); assert!(source_dir.join("guide.md").exists()); @@ -680,6 +726,7 @@ fn test_mv_with_nonexistent_intermediate_path() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!( @@ -717,6 +764,7 @@ fn test_mv_self_reference_cross_directory() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -755,6 +803,7 @@ fn test_mv_error_type_validation() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // This test just verifies that normal operation succeeds @@ -791,6 +840,7 @@ fn test_mv_with_relative_target_path() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -845,6 +895,7 @@ fn test_mv_deep_new_directory_with_links() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -904,6 +955,7 @@ fn test_mv_preserves_internal_anchor_links( target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!( @@ -937,6 +989,7 @@ fn test_mv_with_broken_internal_link() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Should succeed — broken links should be skipped, not cause failure @@ -972,6 +1025,7 @@ fn test_mv_source_equals_dest() { file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Should either succeed as no-op or return an error, but NOT delete the file @@ -1021,10 +1075,18 @@ fn test_mv_same_file_path_variants_are_noops( "relative_to_absolute" => { let abs_path = file.canonicalize().unwrap(); with_current_dir(temp_dir.path(), || { - mv("./test.md", abs_path.to_str().unwrap(), ".", false) + mv( + "./test.md", + abs_path.to_str().unwrap(), + ".", + false, + &NoopProgress, + ) }) } - "dot_slash" => with_current_dir(temp_dir.path(), || mv("./doc.md", "./doc.md", ".", false)), + "dot_slash" => with_current_dir(temp_dir.path(), || { + mv("./doc.md", "./doc.md", ".", false, &NoopProgress) + }), "trailing_slash" => { let path_with_slash = format!("{}/{}", temp_dir.path().to_str().unwrap(), file_name); let path_without_slash = file.to_str().unwrap().to_string(); @@ -1033,6 +1095,7 @@ fn test_mv_same_file_path_variants_are_noops( &path_without_slash, temp_dir.path(), false, + &NoopProgress, ) } _ => unreachable!("unsupported scenario: {scenario}"), @@ -1074,6 +1137,7 @@ fn test_mv_same_path_preserves_references() { source_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Both files should be unchanged @@ -1108,6 +1172,7 @@ fn test_mv_symlink_to_same_file() { real_file.to_str().unwrap(), temp_dir.path(), false, + &NoopProgress, ); // Behavior should be safe - either no-op or well-defined @@ -1150,6 +1215,7 @@ fn test_mv_symbolic_link_reference_updates_reference() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -1175,6 +1241,7 @@ fn test_mv_reference_file_with_crlf_line_endings_preserves_line_endings() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -1223,6 +1290,7 @@ fn test_mv_handles_anchor_link_variants( target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -1262,6 +1330,7 @@ fn test_mv_preserves_anchor_links() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -1292,6 +1361,7 @@ fn test_mv_dry_run_does_not_move() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); @@ -1324,6 +1394,7 @@ fn test_mv_dry_run_does_not_update_references() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); @@ -1353,6 +1424,7 @@ fn test_mv_dry_run_does_not_create_directories() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); @@ -1372,6 +1444,7 @@ fn test_mv_dry_run_validates_source() { temp_dir.path().join("target.md").to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!( @@ -1380,7 +1453,7 @@ fn test_mv_dry_run_validates_source() { ); } -/// Dry-run with rename (same directory, different name) should not modify anything. +/// Dry-run with rename (same directory, different name, &NoopProgress) should not modify anything. #[test] #[allow(clippy::unwrap_used)] fn test_mv_dry_run_rename_scenario() { @@ -1399,6 +1472,7 @@ fn test_mv_dry_run_rename_scenario() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); @@ -1432,6 +1506,7 @@ fn test_mv_dry_run_with_internal_links() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); @@ -1473,6 +1548,7 @@ fn test_mv_destination_already_exists() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); match result { @@ -1512,6 +1588,7 @@ fn test_mv_dry_run_destination_already_exists() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); // Dry-run should also fail for existing destination @@ -1543,6 +1620,7 @@ fn test_mv_to_existing_directory() { target_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Operation should succeed @@ -1590,6 +1668,7 @@ fn test_mv_to_directory_with_references() { target_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -1627,6 +1706,7 @@ fn test_mv_to_directory_updates_internal_links() { target_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -1661,6 +1741,7 @@ fn test_mv_to_nested_existing_directory() { target_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -1690,6 +1771,7 @@ fn test_mv_to_directory_dry_run() { target_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); @@ -1725,6 +1807,7 @@ fn test_mv_to_directory_file_already_exists() { target_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!( @@ -1760,6 +1843,7 @@ fn test_mv_to_directory_with_trailing_slash() { &target_with_slash, temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!( @@ -1789,6 +1873,7 @@ fn test_mv_to_nonexistent_path_still_works() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -1826,6 +1911,7 @@ fn test_mv_rollback_on_write_failure() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Restore permissions for cleanup (TempDir needs to delete files). @@ -1897,6 +1983,7 @@ fn test_mv_rollback_restores_already_modified_files() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Restore permissions for cleanup. @@ -1958,6 +2045,7 @@ fn test_mv_transaction_happy_path_multiple_refs() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!( @@ -2023,6 +2111,7 @@ fn test_mv_rollback_preserves_source_content() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); // Restore permissions for cleanup. @@ -2066,6 +2155,7 @@ fn test_mv_updates_link_reference_definition_in_external_file() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2107,6 +2197,7 @@ fn test_mv_bom_prefixed_reference_definition_updates_reference() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ) .unwrap(); @@ -2133,6 +2224,7 @@ fn test_mv_updates_link_reference_definition_with_title() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2169,6 +2261,7 @@ fn test_mv_updates_link_reference_definition_with_angle_brackets() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2200,6 +2293,7 @@ fn test_mv_updates_link_reference_definition_with_extra_spaces() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2235,6 +2329,7 @@ fn test_mv_mixed_inline_and_reference_definition() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2276,6 +2371,7 @@ fn test_mv_updates_internal_link_reference_definitions() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2309,6 +2405,7 @@ fn test_mv_dry_run_with_link_reference_definitions() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); @@ -2345,6 +2442,7 @@ fn test_mv_multiple_link_reference_definitions_same_file() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2383,6 +2481,7 @@ fn test_mv_unicode_filename( target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2406,6 +2505,7 @@ fn test_mv_chinese_to_subdirectory() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -2422,6 +2522,7 @@ fn test_mv_unicode_updates_references() { fixture.destination.to_str().unwrap(), fixture.root.to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -2456,6 +2557,7 @@ fn test_mv_updates_internal_unicode_links() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok()); @@ -2488,6 +2590,7 @@ fn test_mv_unicode_dry_run() { target_file.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); @@ -2532,6 +2635,7 @@ fn test_mv_directory_updates_image_references() { new_docs_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2580,6 +2684,7 @@ fn test_mv_directory_updates_multiple_resource_types() { static_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2626,6 +2731,7 @@ fn test_mv_directory_updates_nested_image_references() { new_docs_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), false, + &NoopProgress, ); assert!(result.is_ok(), "mv should succeed: {:?}", result.err()); @@ -2662,6 +2768,7 @@ fn test_mv_directory_dry_run_with_images() { new_docs_dir.to_str().unwrap(), temp_dir.path().to_str().unwrap(), true, + &NoopProgress, ); assert!(result.is_ok()); diff --git a/tests/lib_rename_tests.rs b/tests/lib_rename_tests.rs index 6c108f1..f86e79b 100644 --- a/tests/lib_rename_tests.rs +++ b/tests/lib_rename_tests.rs @@ -1,6 +1,6 @@ use std::fs; -use mdref::rename; +use mdref::{NoopProgress, rename}; use rstest::rstest; mod common; @@ -29,7 +29,7 @@ fn test_rename_same_directory_moves_file_to_new_name() { let source = temp_dir.path().join("source.md"); write_file(&source, "# Source File\n\nSome content."); - let result = rename(&source, "renamed.md", temp_dir.path(), false); + let result = rename(&source, "renamed.md", temp_dir.path(), false, &NoopProgress); assert!(result.is_ok()); assert!(!source.exists()); @@ -44,7 +44,14 @@ fn test_rename_preserves_content() { let source = temp_dir.path().join("doc.md"); write_file(&source, content); - rename(&source, "doc_renamed.md", temp_dir.path(), false).unwrap(); + rename( + &source, + "doc_renamed.md", + temp_dir.path(), + false, + &NoopProgress, + ) + .unwrap(); let renamed_path = temp_dir.path().join("doc_renamed.md"); let result_content = read_file(&renamed_path); @@ -62,7 +69,7 @@ fn test_rename_updates_external_references() { let source = fixture.target; let ref_file = fixture.reference; - rename(&source, "updated.md", &fixture.root, false).unwrap(); + rename(&source, "updated.md", &fixture.root, false, &NoopProgress).unwrap(); let ref_content = read_file(&ref_file); assert!(ref_content.contains("updated.md")); @@ -75,7 +82,14 @@ fn test_rename_updates_multiple_external_references() { let fixture = fixture_multi_file_reference(); let source = fixture.target; - rename(&source, "new_target.md", &fixture.root, false).unwrap(); + rename( + &source, + "new_target.md", + &fixture.root, + false, + &NoopProgress, + ) + .unwrap(); let ref1_content = read_file(&fixture.primary_reference); let ref2_content = read_file(&fixture.secondary_reference); @@ -96,7 +110,7 @@ fn test_rename_updates_self_reference() { let source = temp_dir.path().join("page.md"); write_file(&source, "[Self link](page.md)"); - rename(&source, "page_v2.md", temp_dir.path(), false).unwrap(); + rename(&source, "page_v2.md", temp_dir.path(), false, &NoopProgress).unwrap(); let renamed_path = temp_dir.path().join("page_v2.md"); let content = read_file(&renamed_path); @@ -115,7 +129,14 @@ fn test_rename_preserves_internal_links_to_other_files() { let source = temp_dir.path().join("source.md"); write_file(&source, "[Other file](other.md)"); - rename(&source, "source_v2.md", temp_dir.path(), false).unwrap(); + rename( + &source, + "source_v2.md", + temp_dir.path(), + false, + &NoopProgress, + ) + .unwrap(); let renamed_path = temp_dir.path().join("source_v2.md"); let content = read_file(&renamed_path); @@ -136,7 +157,7 @@ fn test_rename_in_subdirectory() { let ref_file = temp_dir.path().join("root.md"); write_file(&ref_file, "[Deep](sub/deep.md)"); - rename(&source, "shallow.md", temp_dir.path(), false).unwrap(); + rename(&source, "shallow.md", temp_dir.path(), false, &NoopProgress).unwrap(); assert!(temp_dir.path().join("sub").join("shallow.md").exists()); @@ -159,7 +180,7 @@ fn test_rename_case_only_name_on_case_insensitive_filesystem_updates_file_and_re let ref_file = temp_dir.path().join("index.md"); write_file(&ref_file, "[Guide](Readme.md)"); - rename(&source, "README.md", temp_dir.path(), false).unwrap(); + rename(&source, "README.md", temp_dir.path(), false, &NoopProgress).unwrap(); let renamed = temp_dir.path().join("README.md"); assert!(renamed.exists()); @@ -190,6 +211,7 @@ fn test_rename_nonexistent_file() { "new.md", temp_dir.path(), false, + &NoopProgress, ); assert!(result.is_err()); } @@ -212,7 +234,7 @@ fn test_rename_unicode_filename( let source = temp_dir.path().join(old_name); write_file(&source, content); - let result = rename(&source, new_name, temp_dir.path(), false); + let result = rename(&source, new_name, temp_dir.path(), false, &NoopProgress); assert!(result.is_ok()); assert!(!source.exists()); @@ -225,7 +247,14 @@ fn test_rename_unicode_filename( fn test_rename_unicode_updates_references() { let fixture = fixture_unicode_paths(); - rename(&fixture.source, "更新文档.md", &fixture.root, false).unwrap(); + rename( + &fixture.source, + "更新文档.md", + &fixture.root, + false, + &NoopProgress, + ) + .unwrap(); let ref_content = read_file(&fixture.reference); assert!(ref_content.contains("更新文档.md"));