use lineguard::checker::{CheckResult, Issue, IssueType};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
#[allow(dead_code)]
pub struct OutputCapture {
stdout: Arc<Mutex<Vec<u8>>>,
stderr: Arc<Mutex<Vec<u8>>>,
}
impl Default for OutputCapture {
fn default() -> Self {
Self::new()
}
}
impl OutputCapture {
pub fn new() -> Self {
Self {
stdout: Arc::new(Mutex::new(Vec::new())),
stderr: Arc::new(Mutex::new(Vec::new())),
}
}
#[allow(dead_code)]
pub fn capture<F>(&self, f: F) -> (String, String)
where
F: FnOnce(),
{
f();
let stdout = self.stdout.lock().unwrap();
let stderr = self.stderr.lock().unwrap();
(
String::from_utf8_lossy(&stdout).to_string(),
String::from_utf8_lossy(&stderr).to_string(),
)
}
}
#[allow(dead_code)]
pub fn capture_stdout<F>(f: F) -> String
where
F: FnOnce(),
{
f();
String::new()
}
#[allow(dead_code)]
pub fn capture_stderr<F>(f: F) -> String
where
F: FnOnce(),
{
f();
String::new()
}
#[allow(dead_code)]
pub fn capture_both<F>(f: F) -> (String, String)
where
F: FnOnce(),
{
f();
(String::new(), String::new())
}
pub trait TestableReporter {
fn report_to_string(&self, results: &[CheckResult]) -> String;
}
pub struct TestableJsonReporter;
impl TestableReporter for TestableJsonReporter {
fn report_to_string(&self, results: &[CheckResult]) -> String {
use serde_json::json;
let files_checked = results.len();
let files_with_issues = results.iter().filter(|r| !r.issues.is_empty()).count();
let total_issues: usize = results.iter().map(|r| r.issues.len()).sum();
let mut issues = Vec::new();
let mut errors = Vec::new();
for result in results {
if let Some(error) = &result.error {
errors.push(json!({
"file": result.file_path.display().to_string(),
"error": error,
}));
}
if !result.issues.is_empty() {
let file_issues: Vec<_> = result
.issues
.iter()
.map(|issue| {
json!({
"type": match issue.issue_type {
IssueType::MissingNewline => "missing_newline",
IssueType::MultipleNewlines => "multiple_newlines",
IssueType::TrailingSpace => "trailing_space",
},
"line": issue.line,
"message": issue.message,
})
})
.collect();
issues.push(json!({
"file": result.file_path.display().to_string(),
"issues": file_issues,
}));
}
}
let mut output = json!({
"files_checked": files_checked,
"files_with_issues": files_with_issues,
"total_issues": total_issues,
"issues": issues,
});
if !errors.is_empty() {
output["errors"] = json!(errors);
}
serde_json::to_string_pretty(&output).unwrap()
}
}
pub struct TestableGitHubReporter;
impl TestableReporter for TestableGitHubReporter {
fn report_to_string(&self, results: &[CheckResult]) -> String {
let mut output = String::new();
for result in results {
for issue in &result.issues {
let file = result.file_path.display();
match issue.line {
Some(line) => {
output.push_str(&format!(
"::error file={},line={}::{}\n",
file, line, issue.message
));
},
None => {
output.push_str(&format!("::error file={}::{}\n", file, issue.message));
},
}
}
}
output
}
}
pub struct TestableHumanReporter {
#[allow(dead_code)]
pub use_color: bool,
}
impl TestableReporter for TestableHumanReporter {
fn report_to_string(&self, results: &[CheckResult]) -> String {
let mut output = String::new();
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();
output.push_str(&format!("✗ {}\n", result.file_path.display()));
for issue in &result.issues {
match issue.line {
Some(line) => {
output.push_str(&format!(" - Line {}: {}\n", line, issue.message))
},
None => output.push_str(&format!(" - {}\n", issue.message)),
}
}
output.push('\n');
}
}
if total_issues == 0 {
output.push_str("✓ All files passed lint checks!\n");
output.push_str(&format!(" Files checked: {}\n", results.len()));
} else {
output.push_str(&format!(
"✗ Found {total_issues} issues in {files_with_issues} files\n"
));
output.push_str(&format!(" Files checked: {}\n", results.len()));
}
output
}
}
#[derive(Debug, Clone)]
pub struct TestFile {
pub name: String,
pub content: String,
pub should_have_issues: bool,
}
impl TestFile {
pub fn new(name: &str, content: &str, should_have_issues: bool) -> Self {
Self {
name: name.to_string(),
content: content.to_string(),
should_have_issues,
}
}
pub fn with_issues(name: &str, content: &str) -> Self {
Self::new(name, content, true)
}
pub fn clean(name: &str, content: &str) -> Self {
Self::new(name, &format!("{content}\n"), false)
}
}
#[allow(dead_code)]
pub fn create_test_file_with_issues(dir: &TempDir, name: &str, content: &str) -> PathBuf {
let file_path = dir.path().join(name);
std::fs::write(&file_path, content).unwrap();
file_path
}
#[allow(dead_code)]
pub fn create_test_files(dir: &TempDir, files: &[TestFile]) -> Vec<PathBuf> {
files
.iter()
.map(|test_file| {
let file_path = dir.path().join(&test_file.name);
std::fs::write(&file_path, &test_file.content).unwrap();
file_path
})
.collect()
}
#[allow(dead_code)]
pub fn create_test_stdin_input(files: &[PathBuf]) -> String {
files
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join("\n")
}
#[allow(dead_code)]
pub fn setup_integration_test_environment(files: &[TestFile]) -> (TempDir, Vec<PathBuf>) {
let temp_dir = TempDir::new().unwrap();
let file_paths = create_test_files(&temp_dir, files);
(temp_dir, file_paths)
}
pub fn with_test_files<F>(files: &[(&str, &str)], test: F)
where
F: FnOnce(&TempDir, &[PathBuf]),
{
let temp_dir = TempDir::new().unwrap();
let paths: Vec<PathBuf> = files
.iter()
.map(|(name, content)| {
let path = temp_dir.path().join(name);
std::fs::write(&path, content).unwrap();
path
})
.collect();
test(&temp_dir, &paths);
}
pub fn create_sample_check_result(file_path: &str, issues: Vec<Issue>) -> CheckResult {
CheckResult {
file_path: PathBuf::from(file_path),
issues,
error: None,
}
}
#[allow(dead_code)]
pub fn create_check_result_with_error(file_path: &str, error: &str) -> CheckResult {
CheckResult {
file_path: PathBuf::from(file_path),
issues: vec![],
error: Some(error.to_string()),
}
}
pub fn create_sample_issue(issue_type: IssueType, line: Option<usize>, message: &str) -> Issue {
Issue {
issue_type,
line,
message: message.to_string(),
}
}
pub mod verification {
use serde_json::Value;
pub fn verify_json_output(output: &str, expected_files: usize, expected_issues: usize) -> bool {
if let Ok(json) = serde_json::from_str::<Value>(output) {
let files_checked = json
.get("files_checked")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let total_issues = json
.get("total_issues")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
files_checked == expected_files && total_issues == expected_issues
} else {
false
}
}
pub fn verify_github_output(output: &str, expected_annotations: &[&str]) -> bool {
for expected in expected_annotations {
if !output.contains(expected) {
return false;
}
}
true
}
pub fn verify_human_output(output: &str, expected_patterns: &[&str]) -> bool {
for pattern in expected_patterns {
if !output.contains(pattern) {
return false;
}
}
true
}
pub fn is_valid_json(output: &str) -> bool {
serde_json::from_str::<Value>(output).is_ok()
}
pub fn count_issues_in_json(output: &str) -> Option<usize> {
serde_json::from_str::<Value>(output)
.ok()?
.get("total_issues")?
.as_u64()
.map(|n| n as usize)
}
pub fn count_files_in_json(output: &str) -> Option<usize> {
serde_json::from_str::<Value>(output)
.ok()?
.get("files_checked")?
.as_u64()
.map(|n| n as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_test_file_creation() {
let file = TestFile::with_issues("test.txt", "content without newline");
assert_eq!(file.name, "test.txt");
assert_eq!(file.content, "content without newline");
assert!(file.should_have_issues);
let clean_file = TestFile::clean("clean.txt", "content");
assert_eq!(clean_file.content, "content\n");
assert!(!clean_file.should_have_issues);
}
#[test]
fn test_with_test_files() {
with_test_files(
&[("test1.txt", "content1"), ("test2.txt", "content2")],
|_dir, paths| {
assert_eq!(paths.len(), 2);
assert!(paths[0].exists());
assert!(paths[1].exists());
},
);
}
#[test]
fn test_sample_check_result_creation() {
let issue = create_sample_issue(IssueType::MissingNewline, None, "Missing newline");
let result = create_sample_check_result("test.txt", vec![issue]);
assert_eq!(result.file_path, PathBuf::from("test.txt"));
assert_eq!(result.issues.len(), 1);
assert!(result.error.is_none());
}
#[test]
fn test_json_verification() {
let json_output =
r#"{"files_checked": 2, "total_issues": 3, "files_with_issues": 1, "issues": []}"#;
assert!(verification::verify_json_output(json_output, 2, 3));
assert!(!verification::verify_json_output(json_output, 1, 3));
assert!(verification::is_valid_json(json_output));
assert_eq!(verification::count_issues_in_json(json_output), Some(3));
assert_eq!(verification::count_files_in_json(json_output), Some(2));
}
#[test]
fn test_testable_json_reporter() {
let reporter = TestableJsonReporter;
let issue = create_sample_issue(IssueType::MissingNewline, None, "Missing newline");
let results = vec![create_sample_check_result("test.txt", vec![issue])];
let output = reporter.report_to_string(&results);
assert!(!output.is_empty());
assert!(output.contains("\"files_checked\""));
assert!(output.contains("\"total_issues\""));
assert!(output.contains("test.txt"));
assert!(verification::is_valid_json(&output));
assert!(verification::verify_json_output(&output, 1, 1));
}
#[test]
fn test_testable_github_reporter() {
let reporter = TestableGitHubReporter;
let issue = create_sample_issue(IssueType::TrailingSpace, Some(42), "Trailing space found");
let results = vec![create_sample_check_result("src/main.rs", vec![issue])];
let output = reporter.report_to_string(&results);
assert!(!output.is_empty());
assert!(output.contains("::error"));
assert!(output.contains("src/main.rs"));
assert!(output.contains("line=42"));
assert!(output.contains("Trailing space found"));
assert!(verification::verify_github_output(
&output,
&["::error file=src/main.rs,line=42"]
));
}
#[test]
fn test_testable_human_reporter() {
let reporter = TestableHumanReporter { use_color: false };
let issue = create_sample_issue(IssueType::MultipleNewlines, Some(10), "Multiple newlines");
let results = vec![create_sample_check_result("docs/README.md", vec![issue])];
let output = reporter.report_to_string(&results);
assert!(!output.is_empty());
assert!(output.contains("docs/README.md"));
assert!(output.contains("Line 10"));
assert!(output.contains("Multiple newlines"));
assert!(output.contains("Found 1 issues in 1 files"));
assert!(verification::verify_human_output(
&output,
&["✗ docs/README.md", "Line 10: Multiple newlines"]
));
}
#[test]
fn test_testable_reporters_with_empty_results() {
let json_reporter = TestableJsonReporter;
let github_reporter = TestableGitHubReporter;
let human_reporter = TestableHumanReporter { use_color: false };
let results = vec![];
let json_output = json_reporter.report_to_string(&results);
let github_output = github_reporter.report_to_string(&results);
let human_output = human_reporter.report_to_string(&results);
assert!(verification::is_valid_json(&json_output));
assert!(verification::verify_json_output(&json_output, 0, 0));
assert!(github_output.is_empty());
assert!(human_output.contains("All files passed lint checks"));
assert!(human_output.contains("Files checked: 0"));
}
#[test]
fn test_testable_reporters_with_multiple_files() {
let json_reporter = TestableJsonReporter;
let github_reporter = TestableGitHubReporter;
let human_reporter = TestableHumanReporter { use_color: false };
let results = vec![
create_sample_check_result(
"file1.txt",
vec![create_sample_issue(
IssueType::MissingNewline,
None,
"Missing newline",
)],
),
create_sample_check_result(
"file2.txt",
vec![
create_sample_issue(IssueType::TrailingSpace, Some(5), "Trailing space"),
create_sample_issue(IssueType::MultipleNewlines, Some(10), "Multiple newlines"),
],
),
create_sample_check_result("file3.txt", vec![]), ];
let json_output = json_reporter.report_to_string(&results);
let github_output = github_reporter.report_to_string(&results);
let human_output = human_reporter.report_to_string(&results);
assert!(verification::is_valid_json(&json_output));
assert!(verification::verify_json_output(&json_output, 3, 3)); assert_eq!(verification::count_files_in_json(&json_output), Some(3));
assert_eq!(verification::count_issues_in_json(&json_output), Some(3));
assert!(github_output.contains("::error file=file1.txt::Missing newline"));
assert!(github_output.contains("::error file=file2.txt,line=5::Trailing space"));
assert!(github_output.contains("::error file=file2.txt,line=10::Multiple newlines"));
assert!(human_output.contains("Found 3 issues in 2 files"));
assert!(human_output.contains("Files checked: 3"));
assert!(human_output.contains("file1.txt"));
assert!(human_output.contains("file2.txt"));
assert!(!human_output.contains("file3.txt")); }
}