use crate::reporter::{Color, ColoredOutput, Output, Reporter, ReporterWithOutput};
use crate::{CheckResult, Issue};
use std::io;
pub struct HumanReporter {
pub use_color: bool,
}
impl HumanReporter {
pub fn new() -> Self {
Self { use_color: false }
}
pub fn with_color() -> Self {
Self { use_color: true }
}
}
impl Default for HumanReporter {
fn default() -> Self {
Self::new()
}
}
impl Reporter for HumanReporter {
fn report(&self, results: &[CheckResult]) {
let mut output = if self.use_color {
crate::reporter::StdOutput::with_color()
} else {
crate::reporter::StdOutput::new()
};
let _ = self.report_to_colored(results, &mut output);
}
}
impl ReporterWithOutput for HumanReporter {
fn report_to(&self, results: &[CheckResult], output: &mut dyn Output) -> io::Result<()> {
struct PlainOutputWrapper<'a>(&'a mut dyn Output);
impl<'a> ColoredOutput for PlainOutputWrapper<'a> {
fn write_colored(&mut self, content: &str, _color: Color) -> io::Result<()> {
self.0.write(content)
}
}
impl<'a> Output for PlainOutputWrapper<'a> {
fn write(&mut self, content: &str) -> io::Result<()> {
self.0.write(content)
}
fn write_line(&mut self, content: &str) -> io::Result<()> {
self.0.write_line(content)
}
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}
let mut wrapper = PlainOutputWrapper(output);
let reporter = HumanReporter { use_color: false };
reporter.report_to_colored(results, &mut wrapper)
}
}
impl HumanReporter {
pub fn report_to_colored(
&self,
results: &[CheckResult],
output: &mut dyn ColoredOutput,
) -> io::Result<()> {
let (total_issues, files_with_issues) = self.report_file_issues(results, output)?;
self.report_summary(total_issues, files_with_issues, results.len(), output)?;
output.flush()?;
Ok(())
}
fn report_file_issues(
&self,
results: &[CheckResult],
output: &mut dyn ColoredOutput,
) -> io::Result<(usize, usize)> {
let mut total_issues = 0;
let mut files_with_issues = 0;
for result in results {
if !result.issues.is_empty() {
files_with_issues += 1;
total_issues += result.issues.len();
self.write_file_header(&result.file_path, output)?;
for issue in &result.issues {
self.write_issue(issue, output)?;
}
output.write_line("")?;
}
}
Ok((total_issues, files_with_issues))
}
fn write_file_header(
&self,
file_path: &std::path::Path,
output: &mut dyn ColoredOutput,
) -> io::Result<()> {
if self.use_color {
output.write_colored("✗", Color::Red)?;
output.write(" ")?;
output.write(&format!("{}", file_path.display()))?;
output.write_line("")?;
} else {
output.write_line(&format!("✗ {}", file_path.display()))?;
}
Ok(())
}
fn write_issue(&self, issue: &Issue, output: &mut dyn ColoredOutput) -> io::Result<()> {
match issue.line {
Some(line) => {
output.write_line(&format!(" - Line {line}: {}", issue.message))?;
},
None => {
output.write_line(&format!(" - {}", issue.message))?;
},
}
Ok(())
}
fn report_summary(
&self,
total_issues: usize,
files_with_issues: usize,
files_checked: usize,
output: &mut dyn ColoredOutput,
) -> io::Result<()> {
if total_issues == 0 {
if self.use_color {
output.write_colored("✓", Color::Green)?;
output.write_line(" All files passed lint checks!")?;
} else {
output.write_line("✓ All files passed lint checks!")?;
}
} else if self.use_color {
output.write_colored("✗", Color::Red)?;
output.write(&format!(
" Found {total_issues} issues in {files_with_issues} files"
))?;
output.write_line("")?;
} else {
output.write_line(&format!(
"✗ Found {total_issues} issues in {files_with_issues} files"
))?;
}
output.write_line(&format!(" Files checked: {files_checked}"))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::mocks::MockOutput;
use crate::{Issue, IssueType};
use std::path::PathBuf;
fn create_test_results() -> Vec<CheckResult> {
vec![
CheckResult {
file_path: PathBuf::from("test1.txt"),
issues: vec![
Issue {
issue_type: IssueType::TrailingSpace,
line: Some(5),
message: "Trailing spaces found".to_string(),
},
Issue {
issue_type: IssueType::MissingNewline,
line: None,
message: "Missing newline at end of file".to_string(),
},
],
error: None,
},
CheckResult {
file_path: PathBuf::from("test2.txt"),
issues: vec![],
error: None,
},
CheckResult {
file_path: PathBuf::from("test3.txt"),
issues: vec![Issue {
issue_type: IssueType::MultipleNewlines,
line: None,
message: "Multiple newlines at end of file".to_string(),
}],
error: None,
},
]
}
#[test]
fn test_human_reporter_no_issues() {
let reporter = HumanReporter::new();
let mut output = MockOutput::new();
let results = vec![
CheckResult {
file_path: PathBuf::from("clean1.txt"),
issues: vec![],
error: None,
},
CheckResult {
file_path: PathBuf::from("clean2.txt"),
issues: vec![],
error: None,
},
];
reporter.report_to(&results, &mut output).unwrap();
let buffer = output.get_output();
assert!(buffer.contains("✓ All files passed lint checks!"));
assert!(buffer.contains("Files checked: 2"));
assert!(!buffer.contains("✗"));
}
#[test]
fn test_human_reporter_with_issues() {
let reporter = HumanReporter::new();
let mut output = MockOutput::new();
let results = create_test_results();
reporter.report_to(&results, &mut output).unwrap();
let buffer = output.get_output();
assert!(buffer.contains("✗ test1.txt"));
assert!(buffer.contains("✗ test3.txt"));
assert!(!buffer.contains("✗ test2.txt"));
assert!(buffer.contains("Line 5: Trailing spaces found"));
assert!(buffer.contains("Missing newline at end of file"));
assert!(buffer.contains("Multiple newlines at end of file"));
assert!(buffer.contains("✗ Found 3 issues in 2 files"));
assert!(buffer.contains("Files checked: 3"));
}
#[test]
fn test_human_reporter_with_color() {
let reporter = HumanReporter::with_color();
let mut output = MockOutput::new();
let results = create_test_results();
reporter.report_to(&results, &mut output).unwrap();
let buffer = output.get_output();
assert!(buffer.contains("✗ test1.txt"));
assert!(buffer.contains("✗ Found 3 issues in 2 files"));
}
#[test]
fn test_human_reporter_legacy_interface() {
let reporter = HumanReporter::new();
let results = create_test_results();
reporter.report(&results);
}
#[test]
fn test_human_reporter_empty_results() {
let reporter = HumanReporter::new();
let mut output = MockOutput::new();
let results = vec![];
reporter.report_to(&results, &mut output).unwrap();
let buffer = output.get_output();
assert!(buffer.contains("✓ All files passed lint checks!"));
assert!(buffer.contains("Files checked: 0"));
}
#[test]
fn test_human_reporter_single_issue() {
let reporter = HumanReporter::new();
let mut output = MockOutput::new();
let results = vec![CheckResult {
file_path: PathBuf::from("single.txt"),
issues: vec![Issue {
issue_type: IssueType::TrailingSpace,
line: Some(10),
message: "Trailing spaces found".to_string(),
}],
error: None,
}];
reporter.report_to(&results, &mut output).unwrap();
let buffer = output.get_output();
assert!(buffer.contains("✗ single.txt"));
assert!(buffer.contains("Line 10: Trailing spaces found"));
assert!(buffer.contains("✗ Found 1 issues in 1 files"));
}
#[test]
fn test_human_reporter_default() {
let reporter = HumanReporter::default();
assert!(!reporter.use_color);
}
#[test]
fn test_human_reporter_report_to_colored_with_issues() {
let reporter = HumanReporter::new();
let mut output = MockOutput::new();
let results = create_test_results();
reporter.report_to_colored(&results, &mut output).unwrap();
let output_str = output.get_output();
assert!(output_str.contains("test1.txt"));
assert!(output_str.contains("Trailing spaces found"));
}
#[test]
fn test_human_reporter_report_to_colored_no_issues() {
let reporter = HumanReporter::new();
let mut output = MockOutput::new();
let results = vec![
CheckResult {
file_path: PathBuf::from("clean1.txt"),
issues: vec![],
error: None,
},
CheckResult {
file_path: PathBuf::from("clean2.txt"),
issues: vec![],
error: None,
},
];
reporter.report_to_colored(&results, &mut output).unwrap();
let output_str = output.get_output();
assert!(output_str.contains("All files passed lint checks!"));
}
#[test]
fn test_human_reporter_legacy_with_color() {
let reporter = HumanReporter { use_color: true };
let results = create_test_results();
reporter.report(&results);
}
#[test]
fn test_human_reporter_multiple_files_no_issues() {
let reporter = HumanReporter::new();
let mut output = MockOutput::new();
let results = vec![
CheckResult {
file_path: PathBuf::from("file1.txt"),
issues: vec![],
error: None,
},
CheckResult {
file_path: PathBuf::from("file2.txt"),
issues: vec![],
error: None,
},
CheckResult {
file_path: PathBuf::from("file3.txt"),
issues: vec![],
error: None,
},
];
reporter.report_to(&results, &mut output).unwrap();
let output_str = output.get_output();
assert!(output_str.contains("✓ All files passed lint checks!"));
assert!(output_str.contains("Files checked: 3"));
}
}