mod common;
use mib_rs::ast::Definition;
use mib_rs::types::{DiagCode, DiagnosticConfig};
use std::path::Path;
use common::{collect_mib_files, corpus_dir, problems_dir};
fn parse_file(path: &Path) -> Vec<mib_rs::ast::Module> {
let content = std::fs::read(path).unwrap();
mib_rs::parser::parse(&content, &DiagnosticConfig::default())
}
fn parse_errors(modules: &[mib_rs::ast::Module]) -> Vec<String> {
modules
.iter()
.flat_map(|m| m.diagnostics.iter())
.filter(|d| d.code == DiagCode::ParseError)
.map(|d| d.message.clone())
.collect()
}
#[test]
fn primary_corpus_no_parse_errors() {
let dir = corpus_dir();
if !dir.exists() {
eprintln!("corpus dir not found, skipping: {}", dir.display());
return;
}
let files = collect_mib_files(&dir);
assert!(!files.is_empty(), "no MIB files found in corpus");
let mut failures = Vec::new();
let mut total_defs = 0;
for path in &files {
let modules = parse_file(path);
total_defs += modules.iter().map(|m| m.body.len()).sum::<usize>();
let errors = parse_errors(&modules);
if !errors.is_empty() {
let rel = path.strip_prefix(&dir).unwrap_or(path);
failures.push(format!(
"{}: {:?}",
rel.display(),
&errors[..errors.len().min(3)]
));
}
}
assert!(
total_defs > 80000,
"expected 80k+ definitions, got {total_defs}"
);
if !failures.is_empty() {
panic!(
"{}/{} files had parse errors:\n{}",
failures.len(),
files.len(),
failures.join("\n")
);
}
}
#[test]
fn problems_corpus_no_parse_errors() {
let dir = problems_dir();
if !dir.exists() {
eprintln!("problems dir not found, skipping: {}", dir.display());
return;
}
let files = collect_mib_files(&dir);
assert!(!files.is_empty(), "no MIB files found in problems corpus");
let mut failures = Vec::new();
for path in &files {
let modules = parse_file(path);
let errors = parse_errors(&modules);
if !errors.is_empty() {
let rel = path.strip_prefix(&dir).unwrap_or(path);
failures.push(format!(
"{}: {:?}",
rel.display(),
&errors[..errors.len().min(3)]
));
}
}
if !failures.is_empty() {
panic!(
"{}/{} problem files had parse errors:\n{}",
failures.len(),
files.len(),
failures.join("\n")
);
}
}
#[test]
fn parse_snmpv2_smi() {
let path = corpus_dir().join("ietf/SNMPv2-SMI.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "SNMPv2-SMI");
assert!(
parse_errors(&modules).is_empty(),
"SNMPv2-SMI had parse errors"
);
let type_assignments: Vec<_> = modules[0]
.body
.iter()
.filter_map(|d| match d {
Definition::TypeAssignment(ta) => Some(ta.name.name.as_str()),
_ => None,
})
.collect();
assert!(
type_assignments.contains(&"IpAddress"),
"should parse IpAddress type assignment"
);
assert!(
type_assignments.contains(&"Counter32"),
"should parse Counter32 type assignment"
);
assert!(
type_assignments.contains(&"Gauge32"),
"should parse Gauge32 type assignment"
);
assert!(
type_assignments.contains(&"TimeTicks"),
"should parse TimeTicks type assignment"
);
assert!(
type_assignments.contains(&"Opaque"),
"should parse Opaque type assignment"
);
assert!(
type_assignments.contains(&"Counter64"),
"should parse Counter64 type assignment"
);
}
#[test]
fn parse_if_mib() {
let path = corpus_dir().join("ietf/IF-MIB.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "IF-MIB");
assert!(parse_errors(&modules).is_empty());
let has_module_identity = modules[0]
.body
.iter()
.any(|d| matches!(d, Definition::ModuleIdentity(_)));
assert!(has_module_identity, "IF-MIB should have MODULE-IDENTITY");
let object_count = modules[0]
.body
.iter()
.filter(|d| matches!(d, Definition::ObjectType(_)))
.count();
assert!(
object_count > 20,
"IF-MIB should have 20+ OBJECT-TYPEs, got {object_count}"
);
}
#[test]
fn parse_snmpv2_tc() {
let path = corpus_dir().join("ietf/SNMPv2-TC.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "SNMPv2-TC");
assert!(parse_errors(&modules).is_empty());
let tc_count = modules[0]
.body
.iter()
.filter(|d| matches!(d, Definition::TextualConvention(_)))
.count();
assert!(tc_count > 5, "SNMPv2-TC should have TCs, got {tc_count}");
}
#[test]
fn parse_snmpv2_conf() {
let path = corpus_dir().join("ietf/SNMPv2-CONF.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "SNMPv2-CONF");
assert!(parse_errors(&modules).is_empty());
let macro_count = modules[0]
.body
.iter()
.filter(|d| matches!(d, Definition::MacroDefinition(_)))
.count();
assert!(
macro_count > 0,
"SNMPv2-CONF should have MACRO definitions, got {macro_count}"
);
}
#[test]
fn parse_rfc1155_smi() {
let path = corpus_dir().join("ietf/RFC1155-SMI.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "RFC1155-SMI");
assert!(parse_errors(&modules).is_empty());
}
#[test]
fn parse_entity_mib() {
let path = corpus_dir().join("ietf/ENTITY-MIB.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "ENTITY-MIB");
assert!(parse_errors(&modules).is_empty());
let _has_agent_caps = modules[0]
.body
.iter()
.any(|d| matches!(d, Definition::AgentCapabilities(_)));
}
#[test]
fn parse_cisco_smi() {
let path = corpus_dir().join("cisco/CISCO-SMI.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "CISCO-SMI");
assert!(parse_errors(&modules).is_empty());
}
#[test]
fn parse_juniper_mibs() {
let dir = corpus_dir().join("juniper");
if !dir.exists() {
return;
}
let files = collect_mib_files(&dir);
for path in &files {
let modules = parse_file(path);
let errors = parse_errors(&modules);
assert!(
errors.is_empty(),
"juniper/{} had parse errors: {:?}",
path.file_name().unwrap().to_string_lossy(),
errors
);
}
}
#[test]
fn parse_multimodule_file() {
let path = problems_dir().join("PROBLEM-MULTIMOD.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert!(
modules.len() >= 2,
"PROBLEM-MULTIMOD should contain multiple modules, got {}",
modules.len()
);
for m in &modules {
assert!(m.name.is_some(), "all modules should parse successfully");
}
}
#[test]
fn parse_fs_mib_tolerates_missing_commas() {
let path = corpus_dir().join("misc/FS-MIB.mib");
if !path.exists() {
return;
}
let modules = parse_file(&path);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name.as_ref().unwrap().name, "FS-MIB");
assert!(
parse_errors(&modules).is_empty(),
"FS-MIB should parse without errors (missing commas tolerated)"
);
assert!(
modules[0].body.len() > 2000,
"expected 2000+ defs, got {}",
modules[0].body.len()
);
}
#[test]
fn type_keyword_as_type_assignment_name() {
let input = br#"TEST-MIB DEFINITIONS ::= BEGIN
IpAddress ::= [APPLICATION 0] IMPLICIT OCTET STRING (SIZE (4))
Counter32 ::= [APPLICATION 1] IMPLICIT INTEGER (0..4294967295)
Gauge32 ::= [APPLICATION 2] IMPLICIT INTEGER (0..4294967295)
TimeTicks ::= [APPLICATION 3] IMPLICIT INTEGER (0..4294967295)
Opaque ::= [APPLICATION 4] IMPLICIT OCTET STRING
Counter64 ::= [APPLICATION 6] IMPLICIT INTEGER (0..18446744073709551615)
Unsigned32 ::= [APPLICATION 2] IMPLICIT INTEGER (0..4294967295)
END
"#;
let modules = mib_rs::parser::parse(input, &DiagnosticConfig::default());
assert_eq!(modules.len(), 1);
assert!(parse_errors(&modules).is_empty());
assert_eq!(modules[0].body.len(), 7);
for def in &modules[0].body {
assert!(
matches!(def, Definition::TypeAssignment(_)),
"expected TypeAssignment, got {:?}",
def.name()
);
}
}
#[test]
fn named_number_missing_comma_tolerated() {
let input = br#"TEST-MIB DEFINITIONS ::= BEGIN
testObj OBJECT-TYPE
SYNTAX INTEGER { alpha(1) beta(2) gamma(3) }
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Missing commas in enum."
::= { test 1 }
END
"#;
let modules = mib_rs::parser::parse(input, &DiagnosticConfig::default());
assert_eq!(modules.len(), 1);
assert!(parse_errors(&modules).is_empty());
match &modules[0].body[0] {
Definition::ObjectType(d) => {
let syntax = d.syntax.as_ref().unwrap();
match &syntax.syntax {
mib_rs::ast::TypeSyntax::IntegerEnum { named_numbers, .. } => {
assert_eq!(named_numbers.len(), 3, "should parse all 3 named numbers");
assert_eq!(named_numbers[0].name.name, "alpha");
assert_eq!(named_numbers[1].name.name, "beta");
assert_eq!(named_numbers[2].name.name, "gamma");
}
other => panic!("expected IntegerEnum, got {:?}", other),
}
}
other => panic!("expected ObjectType, got {:?}", other),
}
}
#[test]
fn bits_named_numbers_missing_comma() {
let input = br#"TEST-MIB DEFINITIONS ::= BEGIN
TestBits ::= TEXTUAL-CONVENTION
STATUS current
DESCRIPTION "Bits with missing commas."
SYNTAX BITS { alpha(0) beta(1) gamma(2) }
END
"#;
let modules = mib_rs::parser::parse(input, &DiagnosticConfig::default());
assert_eq!(modules.len(), 1);
assert!(parse_errors(&modules).is_empty());
match &modules[0].body[0] {
Definition::TextualConvention(d) => match &d.syntax.syntax {
mib_rs::ast::TypeSyntax::Bits { named_bits, .. } => {
assert_eq!(named_bits.len(), 3);
}
other => panic!("expected Bits, got {:?}", other),
},
other => panic!("expected TextualConvention, got {:?}", other),
}
}
#[test]
fn tagged_type_with_constraint() {
let input = br#"TEST-MIB DEFINITIONS ::= BEGIN
TestTagged ::= [APPLICATION 0] IMPLICIT OCTET STRING (SIZE (4))
END
"#;
let modules = mib_rs::parser::parse(input, &DiagnosticConfig::default());
assert_eq!(modules.len(), 1);
assert!(parse_errors(&modules).is_empty());
match &modules[0].body[0] {
Definition::TypeAssignment(d) => {
assert_eq!(d.name.name, "TestTagged");
match &d.syntax {
mib_rs::ast::TypeSyntax::Tagged { underlying, .. } => {
assert!(
matches!(
underlying.as_ref(),
mib_rs::ast::TypeSyntax::Constrained { .. }
),
"expected constrained underlying type"
);
}
other => panic!("expected Tagged, got {:?}", other),
}
}
other => panic!("expected TypeAssignment, got {:?}", other),
}
}
#[test]
fn module_compliance_with_refinements() {
let input = br#"TEST-MIB DEFINITIONS ::= BEGIN
testCompliance MODULE-COMPLIANCE
STATUS current
DESCRIPTION "Test compliance."
MODULE
MANDATORY-GROUPS { testGroup }
GROUP testOptGroup
DESCRIPTION "Optional group."
OBJECT testObj
SYNTAX INTEGER (0..100)
MIN-ACCESS read-only
DESCRIPTION "Object refinement."
::= { test 1 }
END
"#;
let modules = mib_rs::parser::parse(input, &DiagnosticConfig::default());
assert_eq!(modules.len(), 1);
assert!(parse_errors(&modules).is_empty());
match &modules[0].body[0] {
Definition::ModuleCompliance(d) => {
assert_eq!(d.modules.len(), 1);
assert_eq!(d.modules[0].mandatory_groups.len(), 1);
assert_eq!(d.modules[0].compliances.len(), 2);
}
other => panic!("expected ModuleCompliance, got {:?}", other),
}
}
#[test]
fn agent_capabilities_with_variations() {
let input = br#"TEST-MIB DEFINITIONS ::= BEGIN
testAgent AGENT-CAPABILITIES
PRODUCT-RELEASE "1.0"
STATUS current
DESCRIPTION "Test agent."
SUPPORTS IF-MIB
INCLUDES { ifGeneralGroup }
VARIATION ifAdminStatus
SYNTAX INTEGER { up(1), down(2) }
ACCESS read-only
DESCRIPTION "Limited."
::= { test 1 }
END
"#;
let modules = mib_rs::parser::parse(input, &DiagnosticConfig::default());
assert_eq!(modules.len(), 1);
assert!(parse_errors(&modules).is_empty());
match &modules[0].body[0] {
Definition::AgentCapabilities(d) => {
assert_eq!(d.supports.len(), 1);
assert_eq!(d.supports[0].variations.len(), 1);
}
other => panic!("expected AgentCapabilities, got {:?}", other),
}
}
#[test]
fn error_recovery_preserves_subsequent_definitions() {
let input = br#"TEST-MIB DEFINITIONS ::= BEGIN
brokenDef OBJECT-TYPE
SYNTAX !!!GARBAGE!!!
::= { test 1 }
goodDef OBJECT IDENTIFIER ::= { iso 3 }
anotherGood OBJECT-TYPE
SYNTAX INTEGER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Good."
::= { test 2 }
END
"#;
let modules = mib_rs::parser::parse(input, &DiagnosticConfig::default());
assert_eq!(modules.len(), 1);
let good_defs: Vec<_> = modules[0]
.body
.iter()
.filter(|d| !matches!(d, Definition::Error(_)))
.collect();
assert!(
good_defs.len() >= 2,
"should recover and parse 2+ good defs, got {}",
good_defs.len()
);
}
#[test]
fn defval_variants() {
let input = br#"TEST-MIB DEFINITIONS ::= BEGIN
intDef OBJECT-TYPE
SYNTAX INTEGER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Integer DEFVAL."
DEFVAL { 42 }
::= { test 1 }
strDef OBJECT-TYPE
SYNTAX OCTET STRING
MAX-ACCESS read-only
STATUS current
DESCRIPTION "String DEFVAL."
DEFVAL { "hello" }
::= { test 2 }
bitsDef OBJECT-TYPE
SYNTAX BITS { alpha(0), beta(1) }
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Bits DEFVAL."
DEFVAL { { alpha, beta } }
::= { test 3 }
oidDef OBJECT-TYPE
SYNTAX OBJECT IDENTIFIER
MAX-ACCESS read-only
STATUS current
DESCRIPTION "OID DEFVAL."
DEFVAL { { 1 3 6 1 } }
::= { test 4 }
END
"#;
let modules = mib_rs::parser::parse(input, &DiagnosticConfig::default());
assert_eq!(modules.len(), 1);
assert!(parse_errors(&modules).is_empty());
assert_eq!(modules[0].body.len(), 4);
}