use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use textfsm_rust::{Template, Value};
use walkdir::WalkDir;
struct Args {
templates_dir: PathBuf,
tests_dir: PathBuf,
verbose: bool,
}
fn parse_args() -> Args {
let args: Vec<String> = std::env::args().collect();
let mut templates_dir = None;
let mut tests_dir = None;
let mut verbose = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--templates" => {
templates_dir = Some(PathBuf::from(&args[i + 1]));
i += 2;
}
"--tests" => {
tests_dir = Some(PathBuf::from(&args[i + 1]));
i += 2;
}
"--verbose" | "-v" => {
verbose = true;
i += 1;
}
_ => {
i += 1;
}
}
}
Args {
templates_dir: templates_dir.expect("--templates is required"),
tests_dir: tests_dir.expect("--tests is required"),
verbose,
}
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct DynamicRecord(HashMap<String, Value>);
fn main() {
let args = parse_args();
println!("Testing serde deserialization against ntc-templates");
println!("Templates directory: {}", args.templates_dir.display());
println!("Tests directory: {}", args.tests_dir.display());
println!();
let mut total = 0;
let mut passed = 0;
let mut serde_passed = 0;
let mut failures: Vec<String> = Vec::new();
for entry in WalkDir::new(&args.templates_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "textfsm"))
{
let template_path = entry.path();
let template_name = template_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let test_files = find_test_files(&args.tests_dir, template_name);
for test_file in test_files {
total += 1;
match run_test(template_path, &test_file, args.verbose) {
Ok((parse_ok, serde_ok)) => {
if parse_ok {
passed += 1;
}
if serde_ok {
serde_passed += 1;
}
if args.verbose && parse_ok && serde_ok {
println!("PASS: {} - {}", template_name, test_file.display());
}
}
Err(e) => {
if args.verbose {
println!("FAIL: {} - {}: {}", template_name, test_file.display(), e);
}
failures.push(format!("{}: {}", template_name, e));
}
}
}
}
println!("\n========================================");
println!(" Serde Test Results");
println!("========================================");
println!("Total tests: {}", total);
println!("Parse passed: {}", passed);
println!("Serde passed: {}", serde_passed);
println!("Pass Rate: {:.1}%", if total > 0 { (passed as f64 / total as f64) * 100.0 } else { 0.0 });
println!("Serde Pass Rate: {:.1}%", if total > 0 { (serde_passed as f64 / total as f64) * 100.0 } else { 0.0 });
println!("========================================");
if !failures.is_empty() && args.verbose {
println!("\nFirst 10 failures:");
for failure in failures.iter().take(10) {
println!(" - {}", failure);
}
}
}
fn find_test_files(tests_dir: &Path, template_name: &str) -> Vec<PathBuf> {
let mut test_files = Vec::new();
let parts: Vec<&str> = template_name.splitn(3, '_').collect();
if parts.len() < 3 {
return test_files;
}
let vendor_os = format!("{}_{}", parts[0], parts[1]);
let command = parts[2];
let test_dir = tests_dir.join(&vendor_os).join(command);
if test_dir.exists() {
for entry in WalkDir::new(&test_dir)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path().extension().is_some_and(|ext| ext == "yml" || ext == "yaml")
})
{
test_files.push(entry.path().to_path_buf());
}
}
test_files
}
fn run_test(
template_path: &std::path::Path,
test_file: &Path,
verbose: bool,
) -> Result<(bool, bool), String> {
let template_content = fs::read_to_string(template_path)
.map_err(|e| format!("failed to read template: {}", e))?;
let raw_file = test_file.with_extension("raw");
let raw_input = fs::read_to_string(&raw_file)
.map_err(|e| format!("failed to read raw file: {}", e))?;
let template = Template::parse_str(&template_content)
.map_err(|e| format!("failed to parse template: {}", e))?;
let mut parser = template.parser();
let standard_results = parser.parse_text(&raw_input)
.map_err(|e| format!("failed to parse input: {}", e))?;
let parse_ok = true;
parser.reset();
let serde_result: Result<Vec<HashMap<String, Value>>, _> = parser.parse_text_into(&raw_input);
let serde_ok = match &serde_result {
Ok(serde_results) => {
if serde_results.len() != standard_results.len() {
if verbose {
println!(
" Row count mismatch: standard={}, serde={}",
standard_results.len(),
serde_results.len()
);
}
false
} else {
let header = template.header();
let mut all_match = true;
for (row_idx, (std_row, serde_row)) in standard_results.iter().zip(serde_results.iter()).enumerate() {
for (col_idx, std_val) in std_row.iter().enumerate() {
let col_name = header.get(col_idx).unwrap_or(&"unknown").to_lowercase();
if let Some(serde_val) = serde_row.get(&col_name) {
if std_val != serde_val {
if verbose {
println!(
" Value mismatch at row {}, col '{}': std={:?}, serde={:?}",
row_idx, col_name, std_val, serde_val
);
}
all_match = false;
}
} else {
if verbose {
println!(" Missing column '{}' in serde result at row {}", col_name, row_idx);
}
all_match = false;
}
}
}
all_match
}
}
Err(e) => {
if verbose {
println!(" Serde deserialization failed: {}", e);
}
false
}
};
Ok((parse_ok, serde_ok))
}