summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/generator/generation.rs20
-rw-r--r--src/generator/mod.rs268
-rw-r--r--src/logger.rs119
-rw-r--r--src/renderer.rs229
-rw-r--r--src/run.rs5
-rw-r--r--src/util/mod.rs3
-rw-r--r--src/util/test_support.rs40
7 files changed, 505 insertions, 179 deletions
diff --git a/src/generator/generation.rs b/src/generator/generation.rs
index 4235cd2..738373b 100644
--- a/src/generator/generation.rs
+++ b/src/generator/generation.rs
@@ -9,25 +9,27 @@ use crate::{
use super::MarkdownGenerator;
-pub(super) struct MarkdownGeneration<'a, W: Write> {
- logger: Logger,
+pub(super) struct MarkdownGeneration<'a, W: Write, LW: Write> {
+ logger: &'a Logger<LW>,
normalizer: Normalizer,
paths: &'a [&'a Path],
project_title: String,
- renderer: Renderer<W>,
+ renderer: Renderer<'a, W, LW>,
seen_paths: HashSet<NormalizedPath>,
}
-impl<'a, W: Write> MarkdownGeneration<'a, W> {
- pub fn new(generator: MarkdownGenerator<'a>, output: W) -> Result<Self, NormalizeError> {
- let renderer =
- Renderer::new(output, generator.render_options).with_logger(generator.logger);
+impl<'a, W: Write, LW: Write> MarkdownGeneration<'a, W, LW> {
+ pub fn new(
+ generator: MarkdownGenerator<'a>,
+ output: W,
+ logger: &'a Logger<LW>,
+ ) -> Result<Self, NormalizeError> {
Ok(Self {
- logger: generator.logger,
+ logger,
normalizer: Normalizer::new(generator.root, generator.origin_base)?,
paths: generator.paths,
project_title: generator.get_or_derive_project_title().to_string(),
- renderer,
+ renderer: Renderer::new(output, logger, generator.render_options),
seen_paths: HashSet::new(),
})
}
diff --git a/src/generator/mod.rs b/src/generator/mod.rs
index 07bdf1f..81e2420 100644
--- a/src/generator/mod.rs
+++ b/src/generator/mod.rs
@@ -10,7 +10,6 @@ use crate::{
const DEFAULT_PROJECT_NAME: &str = "Project Outline";
pub struct MarkdownGenerator<'a> {
- logger: Logger,
origin_base: &'a Path,
paths: &'a [&'a Path],
project_title: Option<&'a str>,
@@ -21,7 +20,6 @@ pub struct MarkdownGenerator<'a> {
impl<'a> MarkdownGenerator<'a> {
pub fn new(paths: &'a [&'a Path]) -> Self {
MarkdownGenerator {
- logger: Logger::default(),
origin_base: Path::new("."),
paths,
project_title: None,
@@ -45,11 +43,6 @@ impl<'a> MarkdownGenerator<'a> {
self
}
- pub fn with_logger(mut self, logger: Logger) -> Self {
- self.logger = logger;
- self
- }
-
pub fn with_render_options(mut self, render_options: RenderOptions) -> Self {
self.render_options = render_options;
self
@@ -67,16 +60,21 @@ impl<'a> MarkdownGenerator<'a> {
}
}
- pub fn generate<W: Write>(self, output: W) -> Result<(), Box<dyn std::error::Error>> {
- MarkdownGeneration::new(self, output)?.run()
+ pub fn generate<W: Write, LW: Write>(
+ self,
+ output: W,
+ logger: &Logger<LW>,
+ ) -> Result<(), Box<dyn std::error::Error>> {
+ MarkdownGeneration::new(self, output, logger)?.run()
}
- pub fn generate_fenced<W: Write>(
+ pub fn generate_fenced<W: Write, LW: Write>(
self,
mut output: W,
+ logger: &Logger<LW>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut md_output = Vec::new();
- self.generate(&mut md_output)?;
+ self.generate(&mut md_output, logger)?;
let fence = generate_outer_backticks(&md_output);
writeln!(output, "{}markdown", fence)?;
@@ -92,40 +90,32 @@ mod tests {
use tempfile::tempdir;
- use crate::{logger::Logger, renderer::RenderOptions};
+ use crate::{
+ logger::Verbosity,
+ renderer::RenderOptions,
+ util::test_support::{logger_with_shared_log, quiet_logger},
+ };
use super::MarkdownGenerator;
fn generate_to_string(
- paths: &[&Path],
- root: &Path,
- origin: &Path,
- project_title: Option<&str>,
+ generator: MarkdownGenerator,
) -> Result<String, Box<dyn std::error::Error>> {
let mut output = Vec::new();
+ let logger = quiet_logger();
- MarkdownGenerator::new(paths)
- .with_root(root)
- .with_origin(origin)
- .with_project_title(project_title)
- .generate(&mut output)?;
+ generator.generate(&mut output, &logger)?;
Ok(String::from_utf8(output).unwrap())
}
fn generate_fenced_to_string(
- paths: &[&Path],
- root: &Path,
- origin: &Path,
- project_title: Option<&str>,
+ generator: MarkdownGenerator,
) -> Result<String, Box<dyn std::error::Error>> {
let mut output = Vec::new();
+ let logger = quiet_logger();
- MarkdownGenerator::new(paths)
- .with_root(root)
- .with_origin(origin)
- .with_project_title(project_title)
- .generate_fenced(&mut output)?;
+ generator.generate_fenced(&mut output, &logger)?;
Ok(String::from_utf8(output).unwrap())
}
@@ -135,13 +125,11 @@ mod tests {
let temp_dir = tempdir().unwrap();
let paths = [];
- let output = generate_to_string(
- &paths,
- temp_dir.path(),
- temp_dir.path(),
- Some("Project name"),
- )
- .unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"));
+ let output = generate_to_string(generator).unwrap();
assert_eq!(output, "# Project name\n");
}
@@ -152,7 +140,11 @@ mod tests {
let paths = [];
let root = temp_dir.path().join("repo2markdown");
- let output = generate_to_string(&paths, &root, temp_dir.path(), None).unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(&root)
+ .with_origin(temp_dir.path())
+ .with_project_title(None);
+ let output = generate_to_string(generator).unwrap();
assert_eq!(output, "# repo2markdown\n");
}
@@ -164,13 +156,11 @@ mod tests {
let paths = [Path::new("main.rs")];
- let output = generate_to_string(
- &paths,
- temp_dir.path(),
- temp_dir.path(),
- Some("Project name"),
- )
- .unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"));
+ let output = generate_to_string(generator).unwrap();
assert!(output.contains("# Project name\n"));
assert!(output.contains("## File: main.rs"));
@@ -186,13 +176,11 @@ mod tests {
let paths = [Path::new("a.rs"), Path::new("b.rs")];
- let output = generate_to_string(
- &paths,
- temp_dir.path(),
- temp_dir.path(),
- Some("Project name"),
- )
- .unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"));
+ let output = generate_to_string(generator).unwrap();
let a_pos = output.find("## File: a.rs").unwrap();
let b_pos = output.find("## File: b.rs").unwrap();
@@ -212,8 +200,11 @@ mod tests {
let paths = [Path::new("main.rs")];
- let output =
- generate_to_string(&paths, temp_dir.path(), &origin, Some("Project name")).unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(&origin)
+ .with_project_title(Some("Project name"));
+ let output = generate_to_string(generator).unwrap();
assert!(output.contains("## File: src/main.rs"));
assert!(output.contains("fn main() {}"));
@@ -230,7 +221,11 @@ mod tests {
let paths = [Path::new("main.rs")];
- let output = generate_to_string(&paths, &root, &origin, Some("Project name")).unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(&root)
+ .with_origin(&origin)
+ .with_project_title(Some("Project name"));
+ let output = generate_to_string(generator).unwrap();
assert!(output.contains("## File: src/main.rs"));
assert!(output.contains("fn main() {}"));
@@ -244,13 +239,11 @@ mod tests {
let paths = [Path::new("a.rs"), Path::new("a.rs")];
- let output = generate_to_string(
- &paths,
- temp_dir.path(),
- temp_dir.path(),
- Some("Project name"),
- )
- .unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"));
+ let output = generate_to_string(generator).unwrap();
assert_eq!(output.matches("## File: a.rs").count(), 1);
}
@@ -264,13 +257,11 @@ mod tests {
let paths = [Path::new("a.rs"), Path::new("src/../a.rs")];
- let output = generate_to_string(
- &paths,
- temp_dir.path(),
- temp_dir.path(),
- Some("Project name"),
- )
- .unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"));
+ let output = generate_to_string(generator).unwrap();
assert_eq!(output.matches("## File: a.rs").count(), 1);
}
@@ -278,29 +269,21 @@ mod tests {
#[test]
fn uses_render_options_for_large_file_placeholders() {
let temp_dir = tempdir().unwrap();
-
fs::write(temp_dir.path().join("big.txt"), "A".repeat(10)).unwrap();
-
let paths = [Path::new("big.txt")];
- let mut output = Vec::new();
-
let render_options = RenderOptions {
max_file_size: 5,
placeholder_for_binary_files: false,
placeholder_for_large_files: true,
};
- MarkdownGenerator::new(&paths)
+ let generator = MarkdownGenerator::new(&paths)
.with_root(temp_dir.path())
.with_origin(temp_dir.path())
.with_project_title(Some("Project name"))
- .with_render_options(render_options)
- .with_logger(Logger::default())
- .generate(&mut output)
- .unwrap();
-
- let output = String::from_utf8(output).unwrap();
+ .with_render_options(render_options);
+ let output = generate_to_string(generator).unwrap();
assert!(output.contains("## File: big.txt"));
assert!(output.contains("[FILE TOO LARGE]"));
@@ -311,13 +294,11 @@ mod tests {
let temp_dir = tempdir().unwrap();
let paths: [&Path; 0] = [];
- let output = generate_fenced_to_string(
- &paths,
- temp_dir.path(),
- temp_dir.path(),
- Some("Project name"),
- )
- .unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"));
+ let output = generate_fenced_to_string(generator).unwrap();
assert_eq!(output, "```markdown\n# Project name\n```\n");
}
@@ -330,13 +311,11 @@ mod tests {
let paths = [Path::new("main.rs")];
- let output = generate_fenced_to_string(
- &paths,
- temp_dir.path(),
- temp_dir.path(),
- Some("Project name"),
- )
- .unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"));
+ let output = generate_fenced_to_string(generator).unwrap();
assert!(output.starts_with("````markdown\n"));
assert!(output.ends_with("\n````\n"));
@@ -353,16 +332,103 @@ mod tests {
let paths = [Path::new("backticks.rs")];
- let output = generate_fenced_to_string(
- &paths,
- temp_dir.path(),
- temp_dir.path(),
- Some("Project name"),
- )
- .unwrap();
+ let generator = MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"));
+ let output = generate_fenced_to_string(generator).unwrap();
assert!(output.starts_with("``````markdown\n"));
assert!(output.ends_with("\n``````\n"));
assert!(output.contains("`````rust\n````\n`````"));
}
+
+ #[test]
+ fn normal_verbosity_logs_duplicate_paths() {
+ let temp_dir = tempdir().unwrap();
+ fs::write(temp_dir.path().join("a.rs"), "A").unwrap();
+
+ let paths = [Path::new("a.rs"), Path::new("a.rs")];
+
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Normal);
+
+ MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"))
+ .generate(&mut output, &logger)
+ .unwrap();
+
+ assert_eq!(logs.contents(), "Warning: skipping duplicate file: a.rs\n");
+ }
+
+ #[test]
+ fn quiet_verbosity_suppresses_duplicate_path_warning() {
+ let temp_dir = tempdir().unwrap();
+ fs::write(temp_dir.path().join("a.rs"), "A").unwrap();
+
+ let paths = [Path::new("a.rs"), Path::new("a.rs")];
+
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Quiet);
+
+ MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"))
+ .generate(&mut output, &logger)
+ .unwrap();
+
+ assert_eq!(logs.contents(), "");
+ }
+
+ #[test]
+ fn normal_verbosity_logs_lexically_equivalent_duplicate_paths() {
+ let temp_dir = tempdir().unwrap();
+
+ fs::create_dir_all(temp_dir.path().join("src")).unwrap();
+ fs::write(temp_dir.path().join("a.rs"), "A").unwrap();
+
+ let paths = [Path::new("a.rs"), Path::new("src/../a.rs")];
+
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Normal);
+
+ MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"))
+ .generate(&mut output, &logger)
+ .unwrap();
+
+ assert_eq!(logs.contents(), "Warning: skipping duplicate file: a.rs\n");
+ }
+
+ #[test]
+ fn verbose_verbosity_logs_rendered_files_and_duplicates_in_order() {
+ let temp_dir = tempdir().unwrap();
+
+ fs::write(temp_dir.path().join("a.rs"), "A").unwrap();
+ fs::write(temp_dir.path().join("b.rs"), "B").unwrap();
+
+ let paths = [Path::new("a.rs"), Path::new("a.rs"), Path::new("b.rs")];
+
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Verbose);
+
+ MarkdownGenerator::new(&paths)
+ .with_root(temp_dir.path())
+ .with_origin(temp_dir.path())
+ .with_project_title(Some("Project name"))
+ .generate(&mut output, &logger)
+ .unwrap();
+
+ assert_eq!(
+ logs.contents(),
+ "Info: rendering file: a.rs\n\
+ Warning: skipping duplicate file: a.rs\n\
+ Info: rendering file: b.rs\n"
+ );
+ }
}
diff --git a/src/logger.rs b/src/logger.rs
index 0352d61..0a0bdc2 100644
--- a/src/logger.rs
+++ b/src/logger.rs
@@ -1,5 +1,10 @@
//! Minimal stderr logging utilities.
+use std::{
+ cell::RefCell,
+ io::{self, Write},
+};
+
/// Logging verbosity level.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Verbosity {
@@ -12,36 +17,130 @@ pub enum Verbosity {
}
/// Lightweight logger used by renderers and generators.
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub struct Logger {
+#[derive(Debug)]
+pub struct Logger<W: Write> {
verbosity: Verbosity,
+ writer: RefCell<W>,
}
-impl Logger {
+impl Logger<io::Stderr> {
/// Creates a logger with the given verbosity.
pub fn new(verbosity: Verbosity) -> Self {
- Self { verbosity }
+ Self {
+ verbosity,
+ writer: RefCell::new(io::stderr()),
+ }
+ }
+}
+
+impl<W: Write> Logger<W> {
+ pub fn with_writer(verbosity: Verbosity, writer: W) -> Self {
+ Self {
+ verbosity,
+ writer: RefCell::new(writer),
+ }
}
/// Emits a warning unless verbosity is [`Verbosity::Quiet`].
pub fn warn(&self, message: impl AsRef<str>) {
if self.verbosity != Verbosity::Quiet {
- eprintln!("Warning: {}", message.as_ref());
+ let _ = writeln!(self.writer.borrow_mut(), "Warning: {}", message.as_ref());
}
}
/// Emits an informational message only when verbosity is [`Verbosity::Verbose`].
pub fn info(&self, message: impl AsRef<str>) {
if self.verbosity == Verbosity::Verbose {
- eprintln!("Info: {}", message.as_ref());
+ let _ = writeln!(self.writer.borrow_mut(), "Info: {}", message.as_ref());
}
}
}
-impl Default for Logger {
- fn default() -> Self {
- Self {
- verbosity: Verbosity::Quiet,
+#[cfg(test)]
+mod tests {
+ use std::io::{self, Write};
+
+ use super::{Logger, Verbosity};
+
+ fn logged_output(logger: &Logger<Vec<u8>>) -> String {
+ String::from_utf8(logger.writer.borrow().clone()).unwrap()
+ }
+
+ #[test]
+ fn quiet_suppresses_warnings() {
+ let logger = Logger::with_writer(Verbosity::Quiet, Vec::new());
+ logger.warn("skipped file");
+ assert_eq!(logged_output(&logger), "");
+ }
+
+ #[test]
+ fn quiet_suppresses_info_messages() {
+ let logger = Logger::with_writer(Verbosity::Quiet, Vec::new());
+ logger.info("rendering file");
+ assert_eq!(logged_output(&logger), "");
+ }
+
+ #[test]
+ fn normal_writes_warnings() {
+ let logger = Logger::with_writer(Verbosity::Normal, Vec::new());
+ logger.warn("skipped file");
+ assert_eq!(logged_output(&logger), "Warning: skipped file\n");
+ }
+
+ #[test]
+ fn normal_suppresses_info_messages() {
+ let logger = Logger::with_writer(Verbosity::Normal, Vec::new());
+ logger.info("rendering file");
+ assert_eq!(logged_output(&logger), "");
+ }
+
+ #[test]
+ fn verbose_writes_warnings() {
+ let logger = Logger::with_writer(Verbosity::Verbose, Vec::new());
+ logger.warn("skipped file");
+ assert_eq!(logged_output(&logger), "Warning: skipped file\n");
+ }
+
+ #[test]
+ fn verbose_writes_info_messages() {
+ let logger = Logger::with_writer(Verbosity::Verbose, Vec::new());
+ logger.info("rendering file");
+ assert_eq!(logged_output(&logger), "Info: rendering file\n");
+ }
+
+ #[test]
+ fn verbose_preserves_log_order() {
+ let logger = Logger::with_writer(Verbosity::Verbose, Vec::new());
+
+ logger.info("rendering a.rs");
+ logger.warn("skipping duplicate file: a.rs");
+ logger.info("rendering b.rs");
+
+ assert_eq!(
+ logged_output(&logger),
+ "Info: rendering a.rs\n\
+ Warning: skipping duplicate file: a.rs\n\
+ Info: rendering b.rs\n"
+ );
+ }
+
+ struct FailingWriter;
+
+ impl Write for FailingWriter {
+ fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
+ Err(io::Error::other("intentional write failure"))
}
+
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+ }
+
+ #[test]
+ fn logging_ignores_writer_errors() {
+ let logger = Logger::with_writer(Verbosity::Verbose, FailingWriter);
+
+ logger.warn("this should not panic");
+ logger.info("this should not panic");
}
}
diff --git a/src/renderer.rs b/src/renderer.rs
index 1db1215..e89482d 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -22,26 +22,21 @@ pub const DEFAULT_MAX_FILE_SIZE: u64 = 1_000_000;
/// Renders files and project metadata as Markdown.
#[derive(Debug)]
-pub struct Renderer<W: Write> {
+pub struct Renderer<'a, W: Write, LW: Write> {
output: W,
+ logger: &'a Logger<LW>,
max_file_size: u64,
- logger: Logger,
placeholder_for_binary_files: bool,
placeholder_for_large_files: bool,
}
-impl<W: Write> Renderer<W> {
- /// Creates a renderer with default [`RenderOptions`].
- pub fn new_with_defaults(output: W) -> Self {
- Self::new(output, RenderOptions::default())
- }
-
+impl<'a, W: Write, LW: Write> Renderer<'a, W, LW> {
/// Creates a renderer with explicit rendering options.
- pub fn new(output: W, config: RenderOptions) -> Self {
+ pub fn new(output: W, logger: &'a Logger<LW>, config: RenderOptions) -> Self {
Self {
output,
+ logger,
max_file_size: config.max_file_size,
- logger: Logger::default(),
placeholder_for_binary_files: config.placeholder_for_binary_files,
placeholder_for_large_files: config.placeholder_for_large_files,
}
@@ -53,12 +48,6 @@ impl<W: Write> Renderer<W> {
self
}
- /// Sets the logger used for warnings and informational messages.
- pub fn with_logger(mut self, logger: Logger) -> Self {
- self.logger = logger;
- self
- }
-
/// Controls whether skipped binary files are represented by placeholders.
pub fn with_binary_file_placeholder(mut self, placeholder_for_binary_files: bool) -> Self {
self.placeholder_for_binary_files = placeholder_for_binary_files;
@@ -198,21 +187,33 @@ fn human_readable_size(bytes: u64) -> String {
mod tests {
use std::{
ffi::OsStr,
- io::Cursor,
+ io::{Cursor, Write},
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
};
use tempfile::tempdir;
- use crate::normalizer::NormalizedPath;
+ use crate::{
+ logger::{Logger, Verbosity},
+ normalizer::NormalizedPath,
+ util::test_support::{logger_with_shared_log, quiet_logger},
+ };
+
+ use super::{RenderOptions, Renderer, human_readable_size};
- use super::{Renderer, human_readable_size};
+ fn renderer_for_tests<'a, W: Write, LW: Write>(
+ output: W,
+ logger: &'a Logger<LW>,
+ ) -> Renderer<'a, W, LW> {
+ Renderer::new(output, logger, RenderOptions::default())
+ }
#[test]
fn renderer_writes_header() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
renderer.render_header("Project name").unwrap();
@@ -222,193 +223,205 @@ mod tests {
#[test]
fn renderer_renders_single_rust_file_with_language_fence() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("fn main() {}");
renderer.render_file(Path::new("main.rs"), input).unwrap();
- let expected = "\n## File: main.rs\n```rust\nfn main() {}\n```\n";
+ let expected = "\n## File: main.rs\n```rust\nfn main() {}\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn binary_files_are_not_included_in_output_at_all_by_default() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new(&[0x00, 0x01, 0x02, 0xc3]);
renderer.render_file(Path::new("image.png"), input).unwrap();
- let expected = "";
- assert_eq!(String::from_utf8(output).unwrap(), expected);
+ assert_eq!(String::from_utf8(output).unwrap(), "");
}
#[test]
fn renderer_can_place_a_placeholder_for_binary_files() {
let mut output = Vec::new();
+ let logger = quiet_logger();
let mut renderer =
- Renderer::new_with_defaults(&mut output).with_binary_file_placeholder(true);
+ renderer_for_tests(&mut output, &logger).with_binary_file_placeholder(true);
let input = Cursor::new(&[0x00, 0x01, 0x02, 0xc3]);
renderer.render_file(Path::new("image.png"), input).unwrap();
- let expected = "\n## File: image.png\n[BINARY FILE]\n";
+ let expected = "\n## File: image.png\n[BINARY FILE]\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn filename_with_linebreaks_and_invalid_chars_handled_properly() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("fn main() {}");
let filename = Path::new(OsStr::from_bytes(b"jap\xE3\x81\x82dir/some\nma\xc3in.rs"));
renderer.render_file(filename, input).unwrap();
- let expected = "\n## File: japあdir/some\\nma\\xC3in.rs\n```rust\nfn main() {}\n```\n";
+ let expected = "\n## File: japあdir/some\\nma\\xC3in.rs\n```rust\nfn main() {}\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn file_with_backticks_is_handled_safely() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("fn main() { println!(\"``` inside\"); }");
renderer
.render_file(Path::new("example.rs"), input)
.unwrap();
- let expected = "\n## File: example.rs\n````rust\n\
- fn main() { println!(\"``` inside\"); }\n````\n";
+ let expected =
+ "\n## File: example.rs\n````rust\nfn main() { println!(\"``` inside\"); }\n````\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn unknown_filetype_has_no_language_fence() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("hello world");
renderer.render_file(Path::new("readme"), input).unwrap();
- let expected = "\n## File: readme\n```\nhello world\n```\n";
+ let expected = "\n## File: readme\n```\nhello world\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn txt_file_has_no_language_fence() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("hello world");
renderer.render_file(Path::new("hello.txt"), input).unwrap();
- let expected = "\n## File: hello.txt\n```\nhello world\n```\n";
+ let expected = "\n## File: hello.txt\n```\nhello world\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn python_file_has_python_language_fence() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("print('hello')");
renderer.render_file(Path::new("main.py"), input).unwrap();
- let expected = "\n## File: main.py\n```python\nprint('hello')\n```\n";
+ let expected = "\n## File: main.py\n```python\nprint('hello')\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn json_file_has_json_language_fence() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("{\"key\":\"value\"}");
renderer.render_file(Path::new("main.json"), input).unwrap();
- let expected = "\n## File: main.json\n```json\n{\"key\":\"value\"}\n```\n";
+ let expected = "\n## File: main.json\n```json\n{\"key\":\"value\"}\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn uppercase_extension_is_still_detected() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("fn main() {}");
renderer.render_file(Path::new("MAIN.RS"), input).unwrap();
- let expected = "\n## File: MAIN.RS\n```rust\nfn main() {}\n```\n";
+ let expected = "\n## File: MAIN.RS\n```rust\nfn main() {}\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn bash_file_detected_from_shebang() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("#! /bin/bash\necho hello");
renderer.render_file(Path::new("hello"), input).unwrap();
- let expected = "\n## File: hello\n```bash\n#! /bin/bash\necho hello\n```\n";
+ let expected = "\n## File: hello\n```bash\n#! /bin/bash\necho hello\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn python_file_detected_from_shebang() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output);
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger);
let input = Cursor::new("#! /bin/python3\nprint('hello')");
renderer.render_file(Path::new("hello"), input).unwrap();
- let expected = "\n## File: hello\n```python\n#! /bin/python3\nprint('hello')\n```\n";
+ let expected = "\n## File: hello\n```python\n#! /bin/python3\nprint('hello')\n```\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
#[test]
fn large_files_are_not_included_in_output_at_all_by_default() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output).with_max_file_size(5); // smaller than file
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger).with_max_file_size(5);
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("big.txt");
- let content = "A".repeat(10); // 10 bytes -> bigger than the limit
- std::fs::write(&file_path, &content).unwrap();
+ std::fs::write(&file_path, "A".repeat(10)).unwrap();
+
let normalized_path = NormalizedPath {
root_relative: PathBuf::from("big.txt"),
absolute: file_path,
};
renderer.render_path(&normalized_path).unwrap();
- let expected = "";
- assert_eq!(String::from_utf8(output).unwrap(), expected);
+ assert_eq!(String::from_utf8(output).unwrap(), "");
}
#[test]
fn renderer_can_place_a_placeholder_for_large_files() {
let mut output = Vec::new();
- let mut renderer = Renderer::new_with_defaults(&mut output)
+ let logger = quiet_logger();
+ let mut renderer = renderer_for_tests(&mut output, &logger)
.with_large_file_placeholder(true)
- .with_max_file_size(5); // smaller than file
+ .with_max_file_size(5);
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("big.txt");
- let content = "A".repeat(10); // 10 bytes -> bigger than the limit
- std::fs::write(&file_path, &content).unwrap();
+ std::fs::write(&file_path, "A".repeat(10)).unwrap();
+
let normalized_path = NormalizedPath {
root_relative: PathBuf::from("big.txt"),
absolute: file_path,
};
renderer.render_path(&normalized_path).unwrap();
- let expected = "\n## File: big.txt\n[FILE TOO LARGE]\n";
+ let expected = "\n## File: big.txt\n[FILE TOO LARGE]\n";
assert_eq!(String::from_utf8(output).unwrap(), expected);
}
@@ -419,4 +432,108 @@ mod tests {
assert_eq!(human_readable_size(1_048_576), "1.0 MiB");
assert_eq!(human_readable_size(5_242_880), "5.0 MiB");
}
+
+ #[test]
+ fn verbose_verbosity_logs_rendered_text_files() {
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Verbose);
+ let mut renderer = renderer_for_tests(&mut output, &logger);
+
+ renderer
+ .render_file(Path::new("main.rs"), Cursor::new("fn main() {}"))
+ .unwrap();
+
+ assert_eq!(logs.contents(), "Info: rendering file: main.rs\n");
+ }
+
+ #[test]
+ fn normal_verbosity_does_not_log_rendered_text_files() {
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Normal);
+ let mut renderer = renderer_for_tests(&mut output, &logger);
+
+ renderer
+ .render_file(Path::new("main.rs"), Cursor::new("fn main() {}"))
+ .unwrap();
+
+ assert_eq!(logs.contents(), "");
+ }
+
+ #[test]
+ fn normal_verbosity_logs_skipped_binary_files() {
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Normal);
+ let mut renderer = renderer_for_tests(&mut output, &logger);
+
+ renderer
+ .render_file(
+ Path::new("image.png"),
+ Cursor::new(&[0x00, 0x01, 0x02, 0xc3]),
+ )
+ .unwrap();
+
+ assert_eq!(
+ logs.contents(),
+ "Warning: skipping binary file: image.png\n"
+ );
+ }
+
+ #[test]
+ fn quiet_verbosity_suppresses_skipped_binary_file_warning() {
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Quiet);
+ let mut renderer = renderer_for_tests(&mut output, &logger);
+
+ renderer
+ .render_file(
+ Path::new("image.png"),
+ Cursor::new(&[0x00, 0x01, 0x02, 0xc3]),
+ )
+ .unwrap();
+
+ assert_eq!(logs.contents(), "");
+ }
+
+ #[test]
+ fn normal_verbosity_logs_skipped_large_files() {
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Normal);
+ let mut renderer = renderer_for_tests(&mut output, &logger).with_max_file_size(5);
+
+ let temp_dir = tempdir().unwrap();
+ let file_path = temp_dir.path().join("big.txt");
+ std::fs::write(&file_path, "A".repeat(10)).unwrap();
+
+ let normalized_path = NormalizedPath {
+ root_relative: PathBuf::from("big.txt"),
+ absolute: file_path,
+ };
+
+ renderer.render_path(&normalized_path).unwrap();
+
+ assert_eq!(
+ logs.contents(),
+ "Warning: skipping large file: big.txt (10 B > limit 5 B)\n"
+ );
+ }
+
+ #[test]
+ fn quiet_verbosity_suppresses_skipped_large_file_warning() {
+ let mut output = Vec::new();
+ let (logger, logs) = logger_with_shared_log(Verbosity::Quiet);
+ let mut renderer = renderer_for_tests(&mut output, &logger).with_max_file_size(5);
+
+ let temp_dir = tempdir().unwrap();
+ let file_path = temp_dir.path().join("big.txt");
+ std::fs::write(&file_path, "A".repeat(10)).unwrap();
+
+ let normalized_path = NormalizedPath {
+ root_relative: PathBuf::from("big.txt"),
+ absolute: file_path,
+ };
+
+ renderer.render_path(&normalized_path).unwrap();
+
+ assert_eq!(logs.contents(), "");
+ }
}
diff --git a/src/run.rs b/src/run.rs
index c28d7f3..2a51f1b 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -31,16 +31,15 @@ pub fn run<R: Read, W: Write>(
let paths = paths_from_null_separated_bytes(&input_buf);
let md_generator = MarkdownGenerator::new(&paths)
- .with_logger(logger)
.with_origin(&cli.origin)
.with_project_title(cli.name.as_deref())
.with_render_options(cli.render_options())
.with_root(&cli.root);
if cli.fenced {
- md_generator.generate_fenced(output)
+ md_generator.generate_fenced(output, &logger)
} else {
- md_generator.generate(output)
+ md_generator.generate(output, &logger)
}
}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index eda8605..d038ae8 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -2,3 +2,6 @@ pub mod fence;
pub mod language;
pub mod path_display;
pub mod path_list;
+
+#[cfg(test)]
+pub(crate) mod test_support;
diff --git a/src/util/test_support.rs b/src/util/test_support.rs
new file mode 100644
index 0000000..14a7a62
--- /dev/null
+++ b/src/util/test_support.rs
@@ -0,0 +1,40 @@
+use std::{
+ cell::RefCell,
+ io::{self, Write},
+ rc::Rc,
+};
+
+use crate::logger::{Logger, Verbosity};
+
+#[derive(Clone, Debug, Default)]
+pub(crate) struct SharedLog {
+ bytes: Rc<RefCell<Vec<u8>>>,
+}
+
+impl SharedLog {
+ pub(crate) fn contents(&self) -> String {
+ String::from_utf8(self.bytes.borrow().clone()).unwrap()
+ }
+}
+
+impl Write for SharedLog {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ self.bytes.borrow_mut().extend_from_slice(buf);
+ Ok(buf.len())
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
+
+pub(crate) fn quiet_logger() -> Logger<Vec<u8>> {
+ Logger::with_writer(Verbosity::Quiet, Vec::new())
+}
+
+pub(crate) fn logger_with_shared_log(verbosity: Verbosity) -> (Logger<SharedLog>, SharedLog) {
+ let shared_log = SharedLog::default();
+ let logger = Logger::with_writer(verbosity, shared_log.clone());
+
+ (logger, shared_log)
+}