textfsm-rust 0.3.1

Template-based state machine for parsing semi-formatted text based on Google's TextFSM
Documentation
//! Test serde deserialization against ntc-templates.
//!
//! Usage:
//!   cargo run --example test_serde_ntc --features serde -- \
//!     --templates /path/to/ntc-templates/ntc_templates/templates \
//!     --tests /path/to/ntc-templates/tests

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,
    }
}

/// Dynamic record type that can hold any parsed values.
/// Uses Value directly since it now implements Deserialize.
#[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();

    // Find all .textfsm template files
    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");

        // Find corresponding test files
        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));
                }
            }
        }
    }

    // Print summary
    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> {
    // Read template
    let template_content = fs::read_to_string(template_path)
        .map_err(|e| format!("failed to read template: {}", e))?;

    // Get raw input from the .raw file
    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))?;

    // Parse the template
    let template = Template::parse_str(&template_content)
        .map_err(|e| format!("failed to parse template: {}", e))?;

    // Test 1: Standard parsing
    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; // If we got here, parsing succeeded

    // Test 2: Serde deserialization using parse_text_into
    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) => {
            // Compare row counts
            if serde_results.len() != standard_results.len() {
                if verbose {
                    println!(
                        "  Row count mismatch: standard={}, serde={}",
                        standard_results.len(),
                        serde_results.len()
                    );
                }
                false
            } else {
                // Compare values
                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))
}