textfsm-rust 0.3.1

Template-based state machine for parsing semi-formatted text based on Google's TextFSM
Documentation
//! Integration tests using ntc-templates.
//!
//! These tests validate CliTable functionality against the real
//! networktocode/ntc-templates repository.

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use textfsm_rust::{CliTable, Index};

fn ntc_templates_path() -> Option<PathBuf> {
    std::env::var("NTC_TEMPLATES_DIR")
        .ok()
        .map(|dir| Path::new(&dir).join("ntc_templates/templates"))
}

fn ntc_index_path() -> Option<PathBuf> {
    ntc_templates_path().map(|p| p.join("index"))
}

fn have_ntc_templates() -> bool {
    ntc_index_path().is_some_and(|p| p.exists())
}

#[test]
fn test_parse_ntc_index() {
    if !have_ntc_templates() {
        eprintln!("Skipping test: ntc-templates not found");
        return;
    }

    let index = Index::from_file(ntc_index_path().unwrap()).expect("should parse ntc-templates index");

    // The index should have many entries
    assert!(index.entries().len() > 500, "ntc-templates should have 500+ entries");

    // Should have standard columns
    assert!(index.columns().contains(&"Template".to_string()));
    assert!(index.columns().contains(&"Platform".to_string()));
    assert!(index.columns().contains(&"Command".to_string()));

    eprintln!("Parsed {} entries from ntc-templates index", index.entries().len());
}

#[test]
fn test_ntc_template_matching() {
    if !have_ntc_templates() {
        eprintln!("Skipping test: ntc-templates not found");
        return;
    }

    let index = Index::from_file(ntc_index_path().unwrap()).expect("should parse ntc-templates index");

    // Test matching various common commands
    let test_cases = vec![
        ("cisco_ios", "show version", "cisco_ios_show_version.textfsm"),
        ("cisco_ios", "show interfaces", "cisco_ios_show_interfaces.textfsm"),
        ("cisco_ios", "show ip route", "cisco_ios_show_ip_route.textfsm"),
        ("cisco_nxos", "show version", "cisco_nxos_show_version.textfsm"),
        ("arista_eos", "show version", "arista_eos_show_version.textfsm"),
        ("juniper_junos", "show version", "juniper_junos_show_version.textfsm"),
    ];

    for (platform, command, expected_template) in test_cases {
        let mut attrs = HashMap::new();
        attrs.insert("Platform".to_string(), platform.to_string());
        attrs.insert("Command".to_string(), command.to_string());

        let result = index.find_match(&attrs);
        assert!(
            result.is_some(),
            "Should find match for {} '{}'",
            platform,
            command
        );

        let entry = result.unwrap();
        assert!(
            entry.templates().contains(&expected_template.to_string()),
            "Expected template {} for {} '{}', got {:?}",
            expected_template,
            platform,
            command,
            entry.templates()
        );
    }
}

#[test]
fn test_ntc_abbreviated_commands() {
    if !have_ntc_templates() {
        eprintln!("Skipping test: ntc-templates not found");
        return;
    }

    let index = Index::from_file(ntc_index_path().unwrap()).expect("should parse ntc-templates index");

    // Test command abbreviations work
    let test_cases = vec![
        ("cisco_ios", "sh ver"),      // show version
        ("cisco_ios", "sh int"),      // show interfaces
        ("cisco_ios", "sh ip ro"),    // show ip route
        ("arista_eos", "sh ver"),
    ];

    for (platform, command) in test_cases {
        let mut attrs = HashMap::new();
        attrs.insert("Platform".to_string(), platform.to_string());
        attrs.insert("Command".to_string(), command.to_string());

        let result = index.find_match(&attrs);
        assert!(
            result.is_some(),
            "Should find match for abbreviated command {} '{}'",
            platform,
            command
        );
    }
}

#[test]
fn test_clitable_with_ntc_templates() {
    if !have_ntc_templates() {
        eprintln!("Skipping test: ntc-templates not found");
        return;
    }

    let templates_path = ntc_templates_path().unwrap();
    let cli_table = CliTable::new(ntc_index_path().unwrap(), &templates_path)
        .expect("should create CliTable from ntc-templates");

    // Test that find_templates works
    let mut attrs = HashMap::new();
    attrs.insert("Platform".to_string(), "cisco_ios".to_string());
    attrs.insert("Command".to_string(), "show version".to_string());

    let templates = cli_table.find_templates(&attrs).expect("should find templates");
    assert!(!templates.is_empty(), "Should find at least one template");
    assert!(
        templates[0].to_string_lossy().contains("cisco_ios_show_version.textfsm"),
        "Should find cisco_ios_show_version.textfsm"
    );
}

#[test]
fn test_parse_cisco_show_version() {
    if !have_ntc_templates() {
        eprintln!("Skipping test: ntc-templates not found");
        return;
    }

    let templates_path = ntc_templates_path().unwrap();
    let cli_table = CliTable::new(ntc_index_path().unwrap(), &templates_path)
        .expect("should create CliTable from ntc-templates");

    // Sample Cisco IOS show version output
    let cli_output = r#"
Cisco IOS Software, C3750 Software (C3750-IPSERVICESK9-M), Version 15.0(2)SE11, RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2017 by Cisco Systems, Inc.
Compiled Sun 01-Oct-17 06:26 by prod_rel_team

ROM: Bootstrap program is C3750 boot loader
BOOTLDR: C3750 Boot Loader (C3750-HBOOT-M) Version 12.2(44)SE5, RELEASE SOFTWARE (fc1)

Switch uptime is 1 year, 2 weeks, 3 days, 4 hours, 5 minutes
System returned to ROM by power-on
System image file is "flash:c3750-ipservicesk9-mz.150-2.SE11.bin"

cisco WS-C3750G-24TS-1U (PowerPC405) processor (revision K0) with 262144K bytes of memory.
Processor board ID CAT1234A5BC
Last reset from power-on
1 Virtual Ethernet interface
24 Gigabit Ethernet interfaces
The password-recovery mechanism is enabled.

512K bytes of flash-simulated non-volatile configuration memory.
Base ethernet MAC Address       : 00:1B:2B:3C:4D:5E
Motherboard assembly number     : 73-10390-05
Power supply part number        : 341-0029-05
Motherboard serial number       : CAT12345ABC
Power supply serial number      : ABC12345678
Model revision number           : K0
Motherboard revision number     : B0
Model number                    : WS-C3750G-24TS-S1U
System serial number            : CAT1234A5BC
Top Assembly Part Number        : 800-26858-02
Top Assembly Revision Number    : D0
Version ID                      : V04
CLEI Code Number                : ABCD1234567
Hardware Board Revision Number  : 0x06
"#;

    let mut attrs = HashMap::new();
    attrs.insert("Platform".to_string(), "cisco_ios".to_string());
    attrs.insert("Command".to_string(), "show version".to_string());

    let result = cli_table.parse_cmd(cli_output, &attrs);

    match result {
        Ok(table) => {
            eprintln!("Parsed {} rows", table.len());
            eprintln!("Headers: {:?}", table.header());
            if !table.is_empty() {
                eprintln!("First row: {}", table.get(0).unwrap());
            }
            // Should parse at least one row
            assert!(!table.is_empty(), "Should parse at least one row from show version output");
        }
        Err(e) => {
            eprintln!("Parse error (may be expected if template format differs): {}", e);
            // Don't fail - template formats can vary
        }
    }
}

#[test]
fn test_all_templates_count() {
    if !have_ntc_templates() {
        eprintln!("Skipping test: ntc-templates not found");
        return;
    }

    let index = Index::from_file(ntc_index_path().unwrap()).expect("should parse ntc-templates index");
    let all_templates = index.all_templates();

    eprintln!("ntc-templates has {} unique templates", all_templates.len());

    // ntc-templates should have hundreds of templates
    assert!(
        all_templates.len() > 300,
        "Expected 300+ unique templates, got {}",
        all_templates.len()
    );

    // Spot check some expected templates exist
    let expected = vec![
        "cisco_ios_show_version.textfsm",
        "cisco_nxos_show_version.textfsm",
        "arista_eos_show_version.textfsm",
    ];

    for template in expected {
        assert!(
            all_templates.contains(&template),
            "Expected template {} not found",
            template
        );
    }
}