From 98b3ebcc9ab38e54bcbd6a7b1bd4f4f61aa3bd2e Mon Sep 17 00:00:00 2001 From: A Farzat Date: Fri, 12 Jun 2026 03:26:50 +0300 Subject: Rename to better reflect what functions/modules do --- src/fenced_md_generator.rs | 156 ++++++++++++++++++++++++ src/lib.rs | 4 +- src/main.rs | 4 +- src/md_fence_wrapper.rs | 153 ----------------------- src/md_generator.rs | 298 +++++++++++++++++++++++++++++++++++++++++++++ src/run.rs | 298 --------------------------------------------- 6 files changed, 458 insertions(+), 455 deletions(-) create mode 100644 src/fenced_md_generator.rs delete mode 100644 src/md_fence_wrapper.rs create mode 100644 src/md_generator.rs delete mode 100644 src/run.rs diff --git a/src/fenced_md_generator.rs b/src/fenced_md_generator.rs new file mode 100644 index 0000000..078a8cb --- /dev/null +++ b/src/fenced_md_generator.rs @@ -0,0 +1,156 @@ +use std::{ + io::{Read, Write}, + path::Path, +}; + +use crate::{ + logger::Logger, md_generator::generate_markdown_from_paths, + util::fence::generate_outer_backticks, +}; + +pub fn generate_fenced_markdown( + input: R, + mut output: W, + root: &Path, + origin_base: &Path, + project_title: Option<&str>, + logger: Logger, +) -> Result<(), Box> { + let mut md_output = Vec::new(); + generate_markdown_from_paths( + input, + &mut md_output, + root, + origin_base, + project_title, + logger, + )?; + let fence = generate_outer_backticks(&md_output); + writeln!(output, "{}markdown", fence)?; + output.write_all(&md_output)?; + writeln!(output, "{}", fence)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::io::{Cursor, Read, Write}; + use tempfile::tempdir; + + use crate::logger::Logger; + + use super::generate_fenced_markdown; + + fn wrap_with_default_logger( + input: R, + output: W, + root: &std::path::Path, + origin_base: &std::path::Path, + project_title: Option<&str>, + ) -> Result<(), Box> { + let logger = Logger::default(); + generate_fenced_markdown(input, output, root, origin_base, project_title, logger) + } + + #[test] + fn empty_input_produces_wrapped_header() { + let temp_dir = tempdir().unwrap(); + let input = Cursor::new(b""); + let mut output = Vec::new(); + let root = temp_dir.path(); + let origin_base = temp_dir.path(); + + wrap_with_default_logger(input, &mut output, root, origin_base, Some("Project name")) + .unwrap(); + + assert_eq!( + String::from_utf8(output).unwrap(), + "```markdown\n# Project name\n```\n" + ); + } + + #[test] + fn single_file_is_wrapped() { + let temp_dir = tempdir().unwrap(); + let origin_base = temp_dir.path(); + let input = Cursor::new(b"test_main.rs\0"); + let mut output = Vec::new(); + let root = temp_dir.path(); + + fs::write(origin_base.join("test_main.rs"), "fn main() {}").unwrap(); + + wrap_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); + + let output_str = String::from_utf8(output).unwrap(); + // Must contain outer fence + assert!(output_str.starts_with("````markdown\n")); + assert!(output_str.ends_with("\n````\n")); + // Must contain file content + assert!(output_str.contains("## File: test_main.rs")); + assert!(output_str.contains("fn main() {}")); + } + + #[test] + fn multiple_files_are_enveloped_using_only_one_outer_fence_while_each_having_its_own_fence() { + let temp_dir = tempdir().unwrap(); + let origin_base = temp_dir.path(); + let input = Cursor::new(b"a.rs\0b.rs\0"); + let mut output = Vec::new(); + let root = temp_dir.path(); + + fs::write(origin_base.join("a.rs"), "A").unwrap(); + fs::write(origin_base.join("b.rs"), "B").unwrap(); + + wrap_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); + + let output_str = String::from_utf8(output).unwrap(); + // Each inner file must have its own language‑specific fence + assert!(output_str.contains("```rust\nA\n```")); + assert!(output_str.contains("```rust\nB\n```")); + // Only one outer fence at start and end + assert_eq!(output_str.matches("````markdown").count(), 1); + assert_eq!(output_str.matches("\n````\n").count(), 1); + } + + #[test] + fn inner_file_with_four_backticks_causes_outer_fence_of_six_backticks() { + let temp_dir = tempdir().unwrap(); + let origin_base = temp_dir.path(); + let input = Cursor::new(b"backticks.rs\0"); + let mut output = Vec::new(); + let root = temp_dir.path(); + + let content = "````"; // 4 backticks + fs::write(origin_base.join("backticks.rs"), content).unwrap(); + + wrap_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); + + let output_str = String::from_utf8(output).unwrap(); + // Inner fence should be 5 backticks because inner content contains 4 backticks + assert!(output_str.contains("`````rust\n````\n`````")); + // Outer fence must then be 6 backticks + assert!(output_str.starts_with("``````markdown\n")); + assert!(output_str.ends_with("\n``````\n")); + } + + #[test] + fn project_title_with_three_backticks_causes_outer_fence_of_four_backticks() { + let temp_dir = tempdir().unwrap(); + let input = Cursor::new(b""); + let mut output = Vec::new(); + let root = temp_dir.path(); + let origin_base = temp_dir.path(); + + let project_title = "Project ``` name"; + wrap_with_default_logger(input, &mut output, root, origin_base, Some(project_title)) + .unwrap(); + + let output_str = String::from_utf8(output).unwrap(); + // Outer fence must be 4 backticks because the header contains 3 backticks + assert!(output_str.starts_with("````markdown\n")); + assert!(output_str.ends_with("\n````\n")); + // The header line itself should appear unchanged + assert!(output_str.contains("# Project ``` name")); + } +} diff --git a/src/lib.rs b/src/lib.rs index 296f4f4..400b5b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ +pub mod fenced_md_generator; pub mod logger; -pub mod md_fence_wrapper; +pub mod md_generator; pub mod normalizer; pub mod renderer; -pub mod run; mod util; diff --git a/src/main.rs b/src/main.rs index b558ae4..5472198 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use std::{ use repo2markdown::{ logger::{Logger, Verbosity}, - run::run, + md_generator::generate_markdown_from_paths, }; fn main() -> Result<(), Box> { @@ -42,7 +42,7 @@ fn main() -> Result<(), Box> { let stdout = io::stdout(); let logger = Logger::new(Verbosity::Verbose); - run( + generate_markdown_from_paths( stdin.lock(), stdout.lock(), root, diff --git a/src/md_fence_wrapper.rs b/src/md_fence_wrapper.rs deleted file mode 100644 index 2928efa..0000000 --- a/src/md_fence_wrapper.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::{ - io::{Read, Write}, - path::Path, -}; - -use crate::{logger::Logger, run::run, util::fence::generate_outer_backticks}; - -pub fn wrap_in_md_fence( - input: R, - mut output: W, - root: &Path, - origin_base: &Path, - project_title: Option<&str>, - logger: Logger, -) -> Result<(), Box> { - let mut md_output = Vec::new(); - run( - input, - &mut md_output, - root, - origin_base, - project_title, - logger, - )?; - let fence = generate_outer_backticks(&md_output); - writeln!(output, "{}markdown", fence)?; - output.write_all(&md_output)?; - writeln!(output, "{}", fence)?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use std::fs; - use std::io::{Cursor, Read, Write}; - use tempfile::tempdir; - - use crate::logger::Logger; - - use super::wrap_in_md_fence; - - fn wrap_with_default_logger( - input: R, - output: W, - root: &std::path::Path, - origin_base: &std::path::Path, - project_title: Option<&str>, - ) -> Result<(), Box> { - let logger = Logger::default(); - wrap_in_md_fence(input, output, root, origin_base, project_title, logger) - } - - #[test] - fn empty_input_produces_wrapped_header() { - let temp_dir = tempdir().unwrap(); - let input = Cursor::new(b""); - let mut output = Vec::new(); - let root = temp_dir.path(); - let origin_base = temp_dir.path(); - - wrap_with_default_logger(input, &mut output, root, origin_base, Some("Project name")) - .unwrap(); - - assert_eq!( - String::from_utf8(output).unwrap(), - "```markdown\n# Project name\n```\n" - ); - } - - #[test] - fn single_file_is_wrapped() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path(); - let input = Cursor::new(b"test_main.rs\0"); - let mut output = Vec::new(); - let root = temp_dir.path(); - - fs::write(origin_base.join("test_main.rs"), "fn main() {}").unwrap(); - - wrap_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - // Must contain outer fence - assert!(output_str.starts_with("````markdown\n")); - assert!(output_str.ends_with("\n````\n")); - // Must contain file content - assert!(output_str.contains("## File: test_main.rs")); - assert!(output_str.contains("fn main() {}")); - } - - #[test] - fn multiple_files_are_enveloped_using_only_one_outer_fence_while_each_having_its_own_fence() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path(); - let input = Cursor::new(b"a.rs\0b.rs\0"); - let mut output = Vec::new(); - let root = temp_dir.path(); - - fs::write(origin_base.join("a.rs"), "A").unwrap(); - fs::write(origin_base.join("b.rs"), "B").unwrap(); - - wrap_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - // Each inner file must have its own language‑specific fence - assert!(output_str.contains("```rust\nA\n```")); - assert!(output_str.contains("```rust\nB\n```")); - // Only one outer fence at start and end - assert_eq!(output_str.matches("````markdown").count(), 1); - assert_eq!(output_str.matches("\n````\n").count(), 1); - } - - #[test] - fn inner_file_with_four_backticks_causes_outer_fence_of_six_backticks() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path(); - let input = Cursor::new(b"backticks.rs\0"); - let mut output = Vec::new(); - let root = temp_dir.path(); - - let content = "````"; // 4 backticks - fs::write(origin_base.join("backticks.rs"), content).unwrap(); - - wrap_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - // Inner fence should be 5 backticks because inner content contains 4 backticks - assert!(output_str.contains("`````rust\n````\n`````")); - // Outer fence must then be 6 backticks - assert!(output_str.starts_with("``````markdown\n")); - assert!(output_str.ends_with("\n``````\n")); - } - - #[test] - fn project_title_with_three_backticks_causes_outer_fence_of_four_backticks() { - let temp_dir = tempdir().unwrap(); - let input = Cursor::new(b""); - let mut output = Vec::new(); - let root = temp_dir.path(); - let origin_base = temp_dir.path(); - - let project_title = "Project ``` name"; - wrap_with_default_logger(input, &mut output, root, origin_base, Some(project_title)) - .unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - // Outer fence must be 4 backticks because the header contains 3 backticks - assert!(output_str.starts_with("````markdown\n")); - assert!(output_str.ends_with("\n````\n")); - // The header line itself should appear unchanged - assert!(output_str.contains("# Project ``` name")); - } -} diff --git a/src/md_generator.rs b/src/md_generator.rs new file mode 100644 index 0000000..44802af --- /dev/null +++ b/src/md_generator.rs @@ -0,0 +1,298 @@ +use std::{ + collections::HashSet, + ffi::OsStr, + io::{Read, Write}, + os::unix::ffi::OsStrExt, + path::Path, +}; + +use crate::{ + logger::Logger, normalizer::Normalizer, renderer::Renderer, util::path_display::display_path, +}; + +const DEFAULT_PROJECT_NAME: &str = "Project Outline"; + +pub fn generate_markdown_from_paths( + mut input: R, + output: W, + root: &Path, + origin_base: &Path, + project_title: Option<&str>, + logger: Logger, +) -> Result<(), Box> { + let mut buf = Vec::new(); + input.read_to_end(&mut buf)?; + + let normalizer = Normalizer::new(root, origin_base)?; + + let mut renderer = Renderer::new(output).with_logger(logger); + let project_title = project_title.unwrap_or_else(|| derive_project_title(root)); + renderer.render_header(project_title)?; + + let mut seen_paths = HashSet::new(); + for segment in buf.split(|b| *b == 0) { + if segment.is_empty() { + continue; + } + + let path = Path::new(OsStr::from_bytes(segment)); + let normalized_path = normalizer.normalize(path)?; + if !seen_paths.insert(normalized_path.relative.clone()) { + logger.warn(format!( + "skipping duplicate file: {}", + display_path(&normalized_path.relative) + )); + continue; + } + renderer.render_path(&normalized_path)?; + } + Ok(()) +} + +fn derive_project_title(root: &Path) -> &str { + if let Some(os_str_name) = root.file_name() + && let Some(name) = os_str_name.to_str() + { + name + } else { + DEFAULT_PROJECT_NAME + } +} + +#[cfg(test)] +mod tests { + use std::ffi::OsStr; + use std::fs; + use std::io::{Cursor, Read, Write}; + use std::os::unix::ffi::OsStrExt; + use std::path::Path; + + use crate::logger::Logger; + use tempfile::tempdir; + + use super::{DEFAULT_PROJECT_NAME, derive_project_title, generate_markdown_from_paths}; + + fn paths_to_null_sep_bytes(file_paths: &[&Path]) -> Vec { + let mut output = Vec::new(); + for path in file_paths { + output.extend(path.as_os_str().as_encoded_bytes()); + output.push(0); + } + output + } + + fn run_with_default_logger( + input: R, + output: W, + root: &Path, + origin_base: &Path, + project_title: Option<&str>, + ) -> Result<(), Box> { + let logger = Logger::default(); + generate_markdown_from_paths(input, output, root, origin_base, project_title, logger) + } + + #[test] + fn cli_with_empty_input_produces_empty_project_with_specified_project_title() { + let temp_dir = tempdir().unwrap(); + let input = Cursor::new(b""); + let mut output = Vec::new(); + let root = temp_dir.path(); + let origin_base = temp_dir.path(); + + run_with_default_logger(input, &mut output, root, origin_base, Some("Project name")) + .unwrap(); + + assert_eq!(String::from_utf8(output).unwrap(), "# Project name\n"); + } + + #[test] + fn cli_reads_single_file_from_stdin() { + let temp_dir = tempdir().unwrap(); + let origin_base = temp_dir.path(); + let input = Cursor::new(b"test_main.rs\0"); + let mut output = Vec::new(); + let root = temp_dir.path(); + + fs::write(origin_base.join("test_main.rs"), "fn main() {}").unwrap(); + + run_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); + + let output_str = String::from_utf8(output).unwrap(); + + assert!(output_str.contains("## File: test_main.rs")); + assert!(output_str.contains("fn main() {}")); + } + + #[test] + fn cli_reads_multiple_files_in_order() { + let temp_dir = tempdir().unwrap(); + let origin_base = temp_dir.path(); + let input = Cursor::new(b"a.rs\0b.rs\0"); + let mut output = Vec::new(); + let root = temp_dir.path(); + + fs::write(origin_base.join("a.rs"), "A").unwrap(); + fs::write(origin_base.join("b.rs"), "B").unwrap(); + + run_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); + + let output = String::from_utf8(output).unwrap(); + + let a_pos = output.find("a.rs").unwrap(); + let b_pos = output.find("b.rs").unwrap(); + + assert!(a_pos < b_pos); + } + + #[test] + fn cli_normalizes_paths_before_rendering() { + let temp_dir = tempdir().unwrap(); + let origin_base = temp_dir.path(); + let input = Cursor::new(b"test/./main.rs\0"); + let mut output = Vec::new(); + let root = temp_dir.path(); + + let write_dir = temp_dir.path().join("test"); + fs::create_dir_all(&write_dir).unwrap(); + fs::write(write_dir.join("main.rs"), "fn main() {}").unwrap(); + + run_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); + + let output = String::from_utf8(output).unwrap(); + + assert!(output.contains("## File: test/main.rs")); + } + + #[test] + fn cli_reads_from_origin_but_outputs_relative_to_root() { + let temp_dir = tempdir().unwrap(); + let origin_base = temp_dir.path().join("sandbox/src"); + let input = Cursor::new(b"main.rs\0"); + let mut output = Vec::new(); + let root = temp_dir.path().join("project"); + + fs::create_dir_all(&origin_base).unwrap(); + fs::write(origin_base.join("main.rs"), "fn main() {}").unwrap(); + + run_with_default_logger(input, &mut output, &root, &origin_base, None).unwrap(); + + let output = String::from_utf8(output).unwrap(); + + // Must contain file content → proves correct reading + assert!(output.contains("fn main() {}")); + + // Must contain normalized path → proves normalization applied + assert!(output.contains("sandbox/src/main.rs")); + } + + #[test] + fn cli_ignores_origin_when_input_path_is_absolute() { + let temp_dir1 = tempdir().unwrap(); + let temp_dir2 = tempdir().unwrap(); + let origin_base = temp_dir2.path(); + let filepath = temp_dir1.path().join("test_main.rs"); + let input = Cursor::new(paths_to_null_sep_bytes(&[&filepath])); + let mut output = Vec::new(); + let root = temp_dir2.path(); + fs::write(&filepath, "fn main() {}").unwrap(); + + run_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); + + let output = String::from_utf8(output).unwrap(); + + // Must contain file content → proves correct reading + assert!(output.contains("fn main() {}")); + } + + #[test] + fn duplicate_files_in_sequence_are_skipped() { + let temp_dir = tempdir().unwrap(); + let origin = temp_dir.path(); + let root = temp_dir.path(); + + fs::write(origin.join("a.rs"), "A").unwrap(); + + let input = Cursor::new(b"a.rs\0a.rs\0"); + let mut output = Vec::new(); + + run_with_default_logger(input, &mut output, root, origin, None).unwrap(); + + let output = String::from_utf8(output).unwrap(); + + assert_eq!(output.matches("## File: a.rs").count(), 1); + } + + #[test] + fn duplicate_files_are_skipped_with_preserved_display_order_even_if_not_adjacent() { + let temp_dir = tempdir().unwrap(); + let origin = temp_dir.path(); + let root = temp_dir.path(); + + fs::write(origin.join("a.rs"), "A").unwrap(); + fs::write(origin.join("b.rs"), "B").unwrap(); + + let input = Cursor::new(b"a.rs\0b.rs\0a.rs\0"); + let mut output = Vec::new(); + + run_with_default_logger(input, &mut output, root, origin, None).unwrap(); + + let output = String::from_utf8(output).unwrap(); + assert_eq!(output.matches("## File: a.rs").count(), 1); + assert_eq!(output.matches("## File: b.rs").count(), 1); + let a_pos = output.find("a.rs").unwrap(); + let b_pos = output.find("b.rs").unwrap(); + assert!(a_pos < b_pos); + } + + #[test] + fn lexically_equivalent_paths_are_detected_as_duplicates() { + let temp_dir = tempdir().unwrap(); + let origin = temp_dir.path(); + let root = temp_dir.path(); + + fs::create_dir_all(origin.join("bla")).unwrap(); + fs::write(origin.join("a.rs"), "A").unwrap(); + fs::write(origin.join("b.rs"), "B").unwrap(); + + let input = Cursor::new(b"a.rs\0b.rs\0bla/../a.rs\0"); + let mut output = Vec::new(); + + run_with_default_logger(input, &mut output, root, origin, None).unwrap(); + + let output = String::from_utf8(output).unwrap(); + assert_eq!(output.matches("## File: a.rs").count(), 1); + } + + #[test] + fn project_title_is_derived_from_root_by_default_even_if_directory_does_not_exist() { + let temp_dir = tempdir().unwrap(); + let origin_base = temp_dir.path(); + let input = Cursor::new(b""); + let mut output = Vec::new(); + let root = temp_dir.path().join("repo2markdown"); + + run_with_default_logger(input, &mut output, &root, origin_base, None).unwrap(); + + let output_str = String::from_utf8(output).unwrap(); + + assert_eq!(output_str, "# repo2markdown\n"); + } + + #[test] + fn project_title_fallsback_to_default_if_root_is_filesystem_root() { + assert_eq!(derive_project_title(Path::new("/")), DEFAULT_PROJECT_NAME); + } + + #[test] + fn project_title_fallsback_if_root_ending_is_not_utf8() { + let root = Path::new(OsStr::from_bytes(b"/root/fd\xC3")); + assert_eq!(derive_project_title(root), DEFAULT_PROJECT_NAME); + } + + #[test] + fn deriving_project_title_from_root_ignores_trailing_slash() { + let root = Path::new("/root/repo2markdown/"); + assert_eq!(derive_project_title(root), "repo2markdown"); + } +} diff --git a/src/run.rs b/src/run.rs deleted file mode 100644 index c276d7c..0000000 --- a/src/run.rs +++ /dev/null @@ -1,298 +0,0 @@ -use std::{ - collections::HashSet, - ffi::OsStr, - io::{Read, Write}, - os::unix::ffi::OsStrExt, - path::Path, -}; - -use crate::{ - logger::Logger, normalizer::Normalizer, renderer::Renderer, util::path_display::display_path, -}; - -const DEFAULT_PROJECT_NAME: &str = "Project Outline"; - -pub fn run( - mut input: R, - output: W, - root: &Path, - origin_base: &Path, - project_title: Option<&str>, - logger: Logger, -) -> Result<(), Box> { - let mut buf = Vec::new(); - input.read_to_end(&mut buf)?; - - let normalizer = Normalizer::new(root, origin_base)?; - - let mut renderer = Renderer::new(output).with_logger(logger); - let project_title = project_title.unwrap_or_else(|| derive_project_title(root)); - renderer.render_header(project_title)?; - - let mut seen_paths = HashSet::new(); - for segment in buf.split(|b| *b == 0) { - if segment.is_empty() { - continue; - } - - let path = Path::new(OsStr::from_bytes(segment)); - let normalized_path = normalizer.normalize(path)?; - if !seen_paths.insert(normalized_path.relative.clone()) { - logger.warn(format!( - "skipping duplicate file: {}", - display_path(&normalized_path.relative) - )); - continue; - } - renderer.render_path(&normalized_path)?; - } - Ok(()) -} - -fn derive_project_title(root: &Path) -> &str { - if let Some(os_str_name) = root.file_name() - && let Some(name) = os_str_name.to_str() - { - name - } else { - DEFAULT_PROJECT_NAME - } -} - -#[cfg(test)] -mod tests { - use std::ffi::OsStr; - use std::fs; - use std::io::{Cursor, Read, Write}; - use std::os::unix::ffi::OsStrExt; - use std::path::Path; - - use crate::logger::Logger; - use tempfile::tempdir; - - use super::{DEFAULT_PROJECT_NAME, derive_project_title, run}; - - fn paths_to_null_sep_bytes(file_paths: &[&Path]) -> Vec { - let mut output = Vec::new(); - for path in file_paths { - output.extend(path.as_os_str().as_encoded_bytes()); - output.push(0); - } - output - } - - fn run_with_default_logger( - input: R, - output: W, - root: &Path, - origin_base: &Path, - project_title: Option<&str>, - ) -> Result<(), Box> { - let logger = Logger::default(); - run(input, output, root, origin_base, project_title, logger) - } - - #[test] - fn cli_with_empty_input_produces_empty_project_with_specified_project_title() { - let temp_dir = tempdir().unwrap(); - let input = Cursor::new(b""); - let mut output = Vec::new(); - let root = temp_dir.path(); - let origin_base = temp_dir.path(); - - run_with_default_logger(input, &mut output, root, origin_base, Some("Project name")) - .unwrap(); - - assert_eq!(String::from_utf8(output).unwrap(), "# Project name\n"); - } - - #[test] - fn cli_reads_single_file_from_stdin() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path(); - let input = Cursor::new(b"test_main.rs\0"); - let mut output = Vec::new(); - let root = temp_dir.path(); - - fs::write(origin_base.join("test_main.rs"), "fn main() {}").unwrap(); - - run_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - - assert!(output_str.contains("## File: test_main.rs")); - assert!(output_str.contains("fn main() {}")); - } - - #[test] - fn cli_reads_multiple_files_in_order() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path(); - let input = Cursor::new(b"a.rs\0b.rs\0"); - let mut output = Vec::new(); - let root = temp_dir.path(); - - fs::write(origin_base.join("a.rs"), "A").unwrap(); - fs::write(origin_base.join("b.rs"), "B").unwrap(); - - run_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); - - let output = String::from_utf8(output).unwrap(); - - let a_pos = output.find("a.rs").unwrap(); - let b_pos = output.find("b.rs").unwrap(); - - assert!(a_pos < b_pos); - } - - #[test] - fn cli_normalizes_paths_before_rendering() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path(); - let input = Cursor::new(b"test/./main.rs\0"); - let mut output = Vec::new(); - let root = temp_dir.path(); - - let write_dir = temp_dir.path().join("test"); - fs::create_dir_all(&write_dir).unwrap(); - fs::write(write_dir.join("main.rs"), "fn main() {}").unwrap(); - - run_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); - - let output = String::from_utf8(output).unwrap(); - - assert!(output.contains("## File: test/main.rs")); - } - - #[test] - fn cli_reads_from_origin_but_outputs_relative_to_root() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path().join("sandbox/src"); - let input = Cursor::new(b"main.rs\0"); - let mut output = Vec::new(); - let root = temp_dir.path().join("project"); - - fs::create_dir_all(&origin_base).unwrap(); - fs::write(origin_base.join("main.rs"), "fn main() {}").unwrap(); - - run_with_default_logger(input, &mut output, &root, &origin_base, None).unwrap(); - - let output = String::from_utf8(output).unwrap(); - - // Must contain file content → proves correct reading - assert!(output.contains("fn main() {}")); - - // Must contain normalized path → proves normalization applied - assert!(output.contains("sandbox/src/main.rs")); - } - - #[test] - fn cli_ignores_origin_when_input_path_is_absolute() { - let temp_dir1 = tempdir().unwrap(); - let temp_dir2 = tempdir().unwrap(); - let origin_base = temp_dir2.path(); - let filepath = temp_dir1.path().join("test_main.rs"); - let input = Cursor::new(paths_to_null_sep_bytes(&[&filepath])); - let mut output = Vec::new(); - let root = temp_dir2.path(); - fs::write(&filepath, "fn main() {}").unwrap(); - - run_with_default_logger(input, &mut output, root, origin_base, None).unwrap(); - - let output = String::from_utf8(output).unwrap(); - - // Must contain file content → proves correct reading - assert!(output.contains("fn main() {}")); - } - - #[test] - fn duplicate_files_in_sequence_are_skipped() { - let temp_dir = tempdir().unwrap(); - let origin = temp_dir.path(); - let root = temp_dir.path(); - - fs::write(origin.join("a.rs"), "A").unwrap(); - - let input = Cursor::new(b"a.rs\0a.rs\0"); - let mut output = Vec::new(); - - run_with_default_logger(input, &mut output, root, origin, None).unwrap(); - - let output = String::from_utf8(output).unwrap(); - - assert_eq!(output.matches("## File: a.rs").count(), 1); - } - - #[test] - fn duplicate_files_are_skipped_with_preserved_display_order_even_if_not_adjacent() { - let temp_dir = tempdir().unwrap(); - let origin = temp_dir.path(); - let root = temp_dir.path(); - - fs::write(origin.join("a.rs"), "A").unwrap(); - fs::write(origin.join("b.rs"), "B").unwrap(); - - let input = Cursor::new(b"a.rs\0b.rs\0a.rs\0"); - let mut output = Vec::new(); - - run_with_default_logger(input, &mut output, root, origin, None).unwrap(); - - let output = String::from_utf8(output).unwrap(); - assert_eq!(output.matches("## File: a.rs").count(), 1); - assert_eq!(output.matches("## File: b.rs").count(), 1); - let a_pos = output.find("a.rs").unwrap(); - let b_pos = output.find("b.rs").unwrap(); - assert!(a_pos < b_pos); - } - - #[test] - fn lexically_equivalent_paths_are_detected_as_duplicates() { - let temp_dir = tempdir().unwrap(); - let origin = temp_dir.path(); - let root = temp_dir.path(); - - fs::create_dir_all(origin.join("bla")).unwrap(); - fs::write(origin.join("a.rs"), "A").unwrap(); - fs::write(origin.join("b.rs"), "B").unwrap(); - - let input = Cursor::new(b"a.rs\0b.rs\0bla/../a.rs\0"); - let mut output = Vec::new(); - - run_with_default_logger(input, &mut output, root, origin, None).unwrap(); - - let output = String::from_utf8(output).unwrap(); - assert_eq!(output.matches("## File: a.rs").count(), 1); - } - - #[test] - fn project_title_is_derived_from_root_by_default_even_if_directory_does_not_exist() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path(); - let input = Cursor::new(b""); - let mut output = Vec::new(); - let root = temp_dir.path().join("repo2markdown"); - - run_with_default_logger(input, &mut output, &root, origin_base, None).unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - - assert_eq!(output_str, "# repo2markdown\n"); - } - - #[test] - fn project_title_fallsback_to_default_if_root_is_filesystem_root() { - assert_eq!(derive_project_title(Path::new("/")), DEFAULT_PROJECT_NAME); - } - - #[test] - fn project_title_fallsback_if_root_ending_is_not_utf8() { - let root = Path::new(OsStr::from_bytes(b"/root/fd\xC3")); - assert_eq!(derive_project_title(root), DEFAULT_PROJECT_NAME); - } - - #[test] - fn deriving_project_title_from_root_ignores_trailing_slash() { - let root = Path::new("/root/repo2markdown/"); - assert_eq!(derive_project_title(root), "repo2markdown"); - } -} -- cgit v1.3.1