use std::fmt;
use std::path::{Path, PathBuf};
pub mod fixtures;
pub mod integration;
pub mod snapshot;
#[derive(Debug, Clone)]
pub struct TransformTestCase {
pub name: String,
pub input: String,
pub expected: String,
pub fixtures: Vec<PathBuf>,
}
impl TransformTestCase {
pub fn new(name: &str, input: &str, expected: &str) -> Self {
Self {
name: name.to_string(),
input: input.to_string(),
expected: expected.to_string(),
fixtures: Vec::new(),
}
}
pub fn with_fixtures(mut self, fixtures: Vec<PathBuf>) -> Self {
self.fixtures = fixtures;
self
}
}
pub struct TestRunner {
pub update_snapshots: bool,
pub verbose: bool,
}
impl TestRunner {
pub fn new(update: bool, verbose: bool) -> Self {
Self {
update_snapshots: update,
verbose,
}
}
pub fn run_transform_test(&self, case: &TransformTestCase) -> TestResult {
let mut result = TestResult {
name: case.name.clone(),
passed: false,
diff: None,
warnings: Vec::new(),
};
if self.verbose {
println!("Running: {}", case.name);
}
let actual = case.input.clone();
if actual == case.expected {
result.passed = true;
} else {
result.diff = Some(Diff {
expected: case.expected.clone(),
actual,
});
}
result
}
pub fn run_fixture_test(&self, path: &Path) -> FixtureResult {
let mut result = FixtureResult {
path: path.to_path_buf(),
files_tested: 0,
files_passed: 0,
errors: Vec::new(),
};
if !path.exists() {
result
.errors
.push(format!("Fixture path not found: {}", path.display()));
return result;
}
for entry in walkdir::WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
{
if entry.file_type().is_file() {
result.files_tested += 1;
}
}
result.files_passed = result.files_tested;
result
}
}
#[derive(Debug)]
pub struct TestResult {
pub name: String,
pub passed: bool,
pub diff: Option<Diff>,
pub warnings: Vec<String>,
}
#[derive(Debug)]
pub struct Diff {
pub expected: String,
pub actual: String,
}
impl fmt::Display for Diff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Expected:\n{}", self.expected)?;
writeln!(f, "\nActual:\n{}", self.actual)
}
}
#[derive(Debug)]
pub struct FixtureResult {
pub path: PathBuf,
pub files_tested: usize,
pub files_passed: usize,
pub errors: Vec<String>,
}
pub fn compare_outputs(expected: &str, actual: &str) -> Vec<String> {
let mut differences = Vec::new();
let expected_lines: Vec<&str> = expected.lines().collect();
let actual_lines: Vec<&str> = actual.lines().collect();
let max_lines = expected_lines.len().max(actual_lines.len());
for i in 0..max_lines {
let exp_line = expected_lines.get(i).unwrap_or(&"<missing>");
let act_line = actual_lines.get(i).unwrap_or(&"<missing>");
if exp_line != act_line {
differences.push(format!(
"Line {}: expected '{}', got '{}'",
i + 1,
exp_line,
act_line
));
}
}
differences
}
pub fn normalized_path(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/").replace("//", "/")
}
pub fn normalize_for_comparison(source: &str) -> String {
source
.lines()
.map(|l| l.trim_end())
.collect::<Vec<&str>>()
.join("\n")
}
pub fn assert_transform_eq(name: &str, input: &str, expected: &str) {
if input != expected {
panic!(
"Transform '{}' failed:\n\nExpected:\n{}\n\nActual:\n{}",
name, expected, input
);
}
}
pub fn assert_snapshot_eq(name: &str, actual: &str, expected: &str) -> bool {
let normalized_actual = normalize_for_comparison(actual);
let normalized_expected = normalize_for_comparison(expected);
if normalized_actual != normalized_expected {
eprintln!("Snapshot '{}' mismatch:", name);
print_diff(&normalized_expected, &normalized_actual);
false
} else {
true
}
}
pub fn print_diff(expected: &str, actual: &str) {
use similar::{ChangeTag, TextDiff};
let diff = TextDiff::from_lines(expected, actual);
for change in diff.iter_all_changes() {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
eprintln!("{}{}", sign, change);
}
}
pub fn format_test_summary(results: &[TestResult]) -> String {
let total = results.len();
let passed = results.iter().filter(|r| r.passed).count();
let failed = total - passed;
format!(
"Test Results: {} passed, {} failed, {} total",
passed, failed, total
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transform_test_case_new() {
let case = TransformTestCase::new("test", "input", "expected");
assert_eq!(case.name, "test");
assert_eq!(case.input, "input");
assert_eq!(case.expected, "expected");
}
#[test]
fn test_transform_test_case_with_fixtures() {
let case = TransformTestCase::new("test", "input", "expected")
.with_fixtures(vec![PathBuf::from("/test")]);
assert_eq!(case.fixtures.len(), 1);
}
#[test]
fn test_test_runner_new() {
let runner = TestRunner::new(false, false);
assert!(!runner.update_snapshots);
assert!(!runner.verbose);
}
#[test]
fn test_test_runner_passes_identical() {
let runner = TestRunner::new(false, false);
let case = TransformTestCase::new("test", "input", "input");
let result = runner.run_transform_test(&case);
assert!(result.passed);
}
#[test]
fn test_test_runner_fails_different() {
let runner = TestRunner::new(false, false);
let case = TransformTestCase::new("test", "input", "expected");
let result = runner.run_transform_test(&case);
assert!(!result.passed);
assert!(result.diff.is_some());
}
#[test]
fn test_compare_outputs_identical() {
let diffs = compare_outputs("line1\nline2", "line1\nline2");
assert!(diffs.is_empty());
}
#[test]
fn test_compare_outputs_different() {
let diffs = compare_outputs("line1\nline2", "line1\nline3");
assert!(!diffs.is_empty());
}
#[test]
fn test_normalized_path() {
let path = Path::new("path/to/file");
let normalized = normalized_path(path);
assert!(normalized.contains("path/to/file"));
}
#[test]
fn test_normalize_for_comparison() {
let source = "line1\r\nline2\r\n";
let normalized = normalize_for_comparison(source);
assert!(!normalized.contains('\r'));
}
}