use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use textfsm_rust::{ListItem, Template, Value};
use walkdir::WalkDir;
struct TestCase {
raw_path: PathBuf,
yml_path: PathBuf,
template_path: PathBuf,
template_name: String,
test_name: String,
}
enum TestResult {
Pass,
Fail(String),
Error(String),
}
fn discover_test_cases(tests_dir: &Path, templates_dir: &Path) -> Vec<TestCase> {
let mut cases = Vec::new();
let mut yml_paths: Vec<PathBuf> = WalkDir::new(tests_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext == "yml")
})
.map(|e| e.path().to_path_buf())
.collect();
yml_paths.sort();
for yml_path in yml_paths {
let raw_path = yml_path.with_extension("raw");
if !raw_path.exists() {
continue;
}
let rel = match yml_path.strip_prefix(tests_dir) {
Ok(r) => r,
Err(_) => continue,
};
let parts: Vec<&str> = rel
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
if parts.len() < 3 {
continue;
}
let platform = parts[0];
let command = parts[1];
let template_name = format!("{}_{}.textfsm", platform, command);
let template_path = templates_dir.join(&template_name);
if !template_path.exists() {
continue;
}
let test_name = yml_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
cases.push(TestCase {
raw_path,
yml_path,
template_path,
template_name,
test_name,
});
}
cases
}
fn values_match(actual: &Value, expected: &serde_yaml::Value) -> bool {
match (actual, expected) {
(Value::Empty, serde_yaml::Value::Null) => true,
(Value::Empty, serde_yaml::Value::String(s)) => s.is_empty(),
(Value::Single(s), serde_yaml::Value::String(e)) => s == e,
(Value::Single(s), serde_yaml::Value::Number(n)) => *s == n.to_string(),
(Value::Single(s), serde_yaml::Value::Bool(b)) => {
*s == if *b { "True" } else { "False" }
}
(Value::Single(s), serde_yaml::Value::Null) => s.is_empty(),
(Value::List(items), serde_yaml::Value::Sequence(expected_items)) => {
if items.len() != expected_items.len() {
return false;
}
items
.iter()
.zip(expected_items.iter())
.all(|(a, e)| list_item_matches(a, e))
}
(Value::List(items), serde_yaml::Value::Null) => items.is_empty(),
_ => false,
}
}
fn list_item_matches(actual: &ListItem, expected: &serde_yaml::Value) -> bool {
match (actual, expected) {
(ListItem::String(s), serde_yaml::Value::String(e)) => s == e,
(ListItem::String(s), serde_yaml::Value::Number(n)) => *s == n.to_string(),
(ListItem::String(s), serde_yaml::Value::Bool(b)) => {
*s == if *b { "True" } else { "False" }
}
(ListItem::String(s), serde_yaml::Value::Null) => s.is_empty(),
(ListItem::Dict(d), serde_yaml::Value::Mapping(m)) => {
if d.len() != m.len() {
return false;
}
m.iter().all(|(k, v)| {
let key = match k.as_str() {
Some(s) => s,
None => return false,
};
match d.get(key) {
Some(actual_val) => match v {
serde_yaml::Value::String(ev) => actual_val == ev,
serde_yaml::Value::Number(n) => *actual_val == n.to_string(),
serde_yaml::Value::Bool(b) => {
*actual_val == if *b { "True" } else { "False" }
}
serde_yaml::Value::Null => actual_val.is_empty(),
_ => false,
},
None => false,
}
})
}
_ => false,
}
}
fn format_value(val: &Value) -> String {
match val {
Value::Empty => "\"\"".to_string(),
Value::Single(s) => format!("\"{}\"", s),
Value::List(items) => {
let items_str: Vec<String> = items
.iter()
.map(|item| match item {
ListItem::String(s) => format!("\"{}\"", s),
ListItem::Dict(d) => {
let pairs: Vec<String> =
d.iter().map(|(k, v)| format!("'{}': '{}'", k, v)).collect();
format!("{{{}}}", pairs.join(", "))
}
})
.collect();
format!("[{}]", items_str.join(", "))
}
}
}
fn format_yaml_value(val: &serde_yaml::Value) -> String {
match val {
serde_yaml::Value::Null => "\"\"".to_string(),
serde_yaml::Value::Bool(b) => {
format!("\"{}\"", if *b { "True" } else { "False" })
}
serde_yaml::Value::Number(n) => format!("\"{}\"", n),
serde_yaml::Value::String(s) => format!("\"{}\"", s),
serde_yaml::Value::Sequence(items) => {
let items_str: Vec<String> = items.iter().map(format_yaml_value).collect();
format!("[{}]", items_str.join(", "))
}
serde_yaml::Value::Mapping(m) => {
let items_str: Vec<String> = m
.iter()
.map(|(k, v)| format!("{}: {}", format_yaml_value(k), format_yaml_value(v)))
.collect();
format!("{{{}}}", items_str.join(", "))
}
_ => format!("{:?}", val),
}
}
fn run_test(tc: &TestCase) -> TestResult {
let yml_content = match fs::read_to_string(&tc.yml_path) {
Ok(c) => c,
Err(e) => return TestResult::Error(format!("Cannot read YAML: {}", e)),
};
let yaml_data: serde_yaml::Value = match serde_yaml::from_str(&yml_content) {
Ok(d) => d,
Err(e) => return TestResult::Error(format!("YAML parse error: {}", e)),
};
let expected: &Vec<serde_yaml::Value> = match yaml_data.get("parsed_sample") {
Some(serde_yaml::Value::Sequence(items)) => items,
Some(serde_yaml::Value::Null) | None => {
&Vec::new()
}
_ => return TestResult::Error("Invalid parsed_sample format".into()),
};
let template_content = match fs::read_to_string(&tc.template_path) {
Ok(c) => c,
Err(e) => return TestResult::Error(format!("Cannot read template: {}", e)),
};
let template = match Template::parse_str(&template_content) {
Ok(t) => t,
Err(e) => return TestResult::Error(format!("Template parse error: {}", e)),
};
let raw_content = match fs::read_to_string(&tc.raw_path) {
Ok(c) => c,
Err(e) => return TestResult::Error(format!("Cannot read raw input: {}", e)),
};
let mut parser = template.parser();
let results = match parser.parse_text(&raw_content) {
Ok(r) => r,
Err(e) => return TestResult::Error(format!("Parse error: {}", e)),
};
let header: Vec<String> = template.header().iter().map(|s| s.to_lowercase()).collect();
if results.len() != expected.len() {
return TestResult::Fail(format!(
"row count: got {}, expected {}",
results.len(),
expected.len()
));
}
for (row_idx, (actual_row, expected_item)) in
results.iter().zip(expected.iter()).enumerate()
{
let expected_map = match expected_item.as_mapping() {
Some(m) => m,
None => {
return TestResult::Error(format!(
"Expected row {} is not a mapping",
row_idx
))
}
};
for (yml_key, yml_val) in expected_map {
let col_name = match yml_key.as_str() {
Some(s) => s.to_lowercase(),
None => continue,
};
let actual_val = match header.iter().position(|h| *h == col_name) {
Some(idx) => &actual_row[idx],
None => &Value::Empty,
};
if !values_match(actual_val, yml_val) {
return TestResult::Fail(format!(
"row {}, col '{}': got {}, expected {}",
row_idx,
col_name,
format_value(actual_val),
format_yaml_value(yml_val),
));
}
}
}
TestResult::Pass
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: strict_ntc_test /path/to/ntc-templates");
std::process::exit(1);
}
let ntc_dir = PathBuf::from(&args[1]);
let templates_dir = ntc_dir.join("ntc_templates").join("templates");
let tests_dir = ntc_dir.join("tests");
if !templates_dir.exists() {
eprintln!("Error: Templates directory not found: {:?}", templates_dir);
std::process::exit(1);
}
if !tests_dir.exists() {
eprintln!("Error: Tests directory not found: {:?}", tests_dir);
std::process::exit(1);
}
let test_cases = discover_test_cases(&tests_dir, &templates_dir);
println!("Discovered {} test cases\n", test_cases.len());
let mut passed: Vec<&TestCase> = Vec::new();
let mut failed: Vec<(&TestCase, String)> = Vec::new();
let mut errors: Vec<(&TestCase, String)> = Vec::new();
for tc in &test_cases {
match run_test(tc) {
TestResult::Pass => passed.push(tc),
TestResult::Fail(msg) => failed.push((tc, msg)),
TestResult::Error(msg) => errors.push((tc, msg)),
}
}
let total = test_cases.len();
let pass_rate = if total > 0 {
passed.len() as f64 / total as f64 * 100.0
} else {
0.0
};
println!("{}", "=".repeat(60));
println!(" Strict NTC Template Test Results (Rust textfsm-rs)");
println!("{}", "=".repeat(60));
println!("Total: {}", total);
println!("Passed: {}", passed.len());
println!("Failed: {}", failed.len());
println!("Errors: {}", errors.len());
println!("Pass Rate: {:.2}%", pass_rate);
println!();
if !errors.is_empty() {
println!("--- Errors ({}) ---", errors.len());
for (tc, msg) in &errors {
println!(" {} [{}]: {}", tc.template_name, tc.test_name, msg);
}
println!();
}
if !failed.is_empty() {
println!("--- Failures ({}) ---", failed.len());
for (tc, msg) in &failed {
println!(" {} [{}]: {}", tc.template_name, tc.test_name, msg);
}
println!();
}
println!("{}", "=".repeat(60));
if !failed.is_empty() || !errors.is_empty() {
std::process::exit(1);
}
}