towerops/test/snmpkit/snmp_lib/mib/parser_test.exs
2026-06-15 11:15:46 -05:00

698 lines
18 KiB
Elixir

defmodule SnmpKit.SnmpLib.MIB.ParserTest do
use ExUnit.Case, async: true
alias SnmpKit.SnmpLib.MIB.Parser
describe "init_parser/0" do
@tag :yecc_required
test "returns ok tuple with module name" do
result = Parser.init_parser()
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "returns same module name on repeated calls" do
{:ok, module1} = Parser.init_parser()
{:ok, module2} = Parser.init_parser()
assert module1 == module2
end
end
describe "tokenize/1" do
test "tokenizes empty string" do
result = Parser.tokenize("")
assert {:ok, tokens} = result
refute tokens == []
end
test "tokenizes simple MIB header" do
mib = "TEST-MIB DEFINITIONS ::= BEGIN END"
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
refute Enum.empty?(tokens)
end
test "tokenizes reserved words" do
mib = "DEFINITIONS BEGIN END IMPORTS FROM OBJECT-TYPE"
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
assert length(tokens) >= 6
end
test "tokenizes identifiers" do
mib = "testObject myIdentifier"
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
assert length(tokens) >= 2
end
test "tokenizes numbers" do
mib = "123 456 789"
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
assert length(tokens) >= 3
end
test "tokenizes special characters" do
mib = "::= { } ( ) , ;"
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
assert length(tokens) > 5
end
test "tokenizes complete OBJECT-TYPE" do
mib = """
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Test"
::= { test 1 }
"""
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
assert length(tokens) > 10
end
test "handles multiline input" do
mib = """
TEST-MIB
DEFINITIONS
::=
BEGIN
END
"""
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
assert length(tokens) > 4
end
test "handles comments" do
mib = "test -- this is a comment\nOBJECT-TYPE"
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
assert length(tokens) >= 2
end
test "handles hex atoms (8+ characters)" do
mib = "12345678 abcdef01"
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
refute Enum.empty?(tokens)
end
test "handles short hex-like identifiers" do
# Short hex patterns like d1, d2 should remain as atoms
mib = "d1 d2 a1"
result = Parser.tokenize(mib)
assert {:ok, tokens} = result
refute Enum.empty?(tokens)
end
end
describe "parse/1" do
@tag :yecc_required
test "returns error for empty string" do
result = Parser.parse("")
assert {:error, _reason} = result
end
@tag :yecc_required
test "returns error for invalid MIB content" do
result = Parser.parse("invalid mib content")
assert {:error, _reason} = result
end
@tag :yecc_required
test "accepts minimal MIB header" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses MIB with IMPORTS" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY FROM SNMPv2-SMI;
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses MIB with OBJECT-TYPE" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Test object"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "returns structured error on parse failure" do
result = Parser.parse("BEGIN END")
assert {:error, _reason} = result
end
end
describe "parse_tokens/1" do
@tag :yecc_required
test "accepts token list" do
# Minimal token list representing "TEST-MIB DEFINITIONS ::= BEGIN END"
tokens = [
{:variable, 1, :TEST_MIB},
{:DEFINITIONS, 1},
{:"::=", 1},
{:BEGIN, 1},
{:END, 1},
{:"$end", 1}
]
result = Parser.parse_tokens(tokens)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "returns error for invalid token sequence" do
tokens = [{:BEGIN, 1}, {:END, 1}]
result = Parser.parse_tokens(tokens)
assert {:error, _reason} = result
end
@tag :yecc_required
test "accepts empty token list" do
tokens = []
result = Parser.parse_tokens(tokens)
assert {:error, _reason} = result
end
end
describe "mibdirs/1" do
@tag :yecc_required
test "accepts empty directory list" do
result = Parser.mibdirs([])
assert map_size(result) == 0
end
@tag :yecc_required
test "returns error for non-existent directory" do
result = Parser.mibdirs(["/nonexistent/path/to/mibs"])
assert Map.has_key?(result, "/nonexistent/path/to/mibs")
end
@tag :yecc_required
test "processes multiple directories" do
dirs = ["/tmp/mibs1", "/tmp/mibs2"]
result = Parser.mibdirs(dirs)
assert map_size(result) == 2
end
@tag :yecc_required
test "result contains directory structure" do
result = Parser.mibdirs(["/nonexistent"])
dir_result = result["/nonexistent"]
assert Map.has_key?(dir_result, :directory)
assert Map.has_key?(dir_result, :total)
assert Map.has_key?(dir_result, :success)
assert Map.has_key?(dir_result, :failures)
end
end
describe "tokenize integration" do
test "converts long hex atoms to integers" do
# Tokenize content with long hex pattern (8+ characters)
mib = "12345678"
{:ok, tokens} = Parser.tokenize(mib)
# Should convert long hex atom to integer token
assert Enum.any?(tokens, fn
{:integer, _, _} -> true
_ -> false
end)
end
test "preserves short hex-like identifiers as atoms" do
# Short patterns should remain as atoms
mib = "d1"
{:ok, tokens} = Parser.tokenize(mib)
# Should NOT convert short patterns to integers
assert Enum.any?(tokens, fn
{:atom, _, _} -> true
_ -> false
end)
end
test "handles mixed hex and regular identifiers" do
mib = "test 12345678 identifier"
{:ok, tokens} = Parser.tokenize(mib)
assert length(tokens) >= 3
end
end
describe "error handling" do
@tag :yecc_required
test "parse handles malformed syntax gracefully" do
malformed = "OBJECT-TYPE ::= {{{ test"
result = Parser.parse(malformed)
assert {:error, _reason} = result
end
@tag :yecc_required
test "parse handles incomplete MIB" do
incomplete = "TEST-MIB DEFINITIONS ::= BEGIN"
result = Parser.parse(incomplete)
assert {:error, _reason} = result
end
@tag :yecc_required
test "parse handles missing keywords" do
missing_keyword = "TEST-MIB ::= BEGIN END"
result = Parser.parse(missing_keyword)
assert {:error, _reason} = result
end
test "tokenize handles special characters" do
special = "@#$%^&*"
result = Parser.tokenize(special)
# Should either succeed with tokens or return error
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
end
describe "MIB structure parsing" do
@tag :yecc_required
test "parses MODULE-IDENTITY" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testMIB MODULE-IDENTITY
LAST-UPDATED "202501010000Z"
ORGANIZATION "Test Org"
CONTACT-INFO "test@example.com"
DESCRIPTION "Test MIB"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses TEXTUAL-CONVENTION" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
TestTC ::= TEXTUAL-CONVENTION
STATUS current
DESCRIPTION "Test textual convention"
SYNTAX Integer32
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses OBJECT-GROUP" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testGroup OBJECT-GROUP
OBJECTS { testObject1, testObject2 }
STATUS current
DESCRIPTION "Test object group"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses EXPORTS clause" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
EXPORTS testObject, testGroup;
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
end
describe "complex MIB parsing" do
@tag :yecc_required
test "parses MIB with multiple IMPORTS" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32
FROM SNMPv2-SMI
TEXTUAL-CONVENTION FROM SNMPv2-TC;
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses MIB with SEQUENCE OF" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
TestTable ::= SEQUENCE OF TestEntry
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses OBJECT-TYPE with INDEX" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testEntry OBJECT-TYPE
SYNTAX TestEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "Test entry"
INDEX { testIndex }
::= { testTable 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses OBJECT-TYPE with SIZE constraint" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX OCTET STRING (SIZE (0..255))
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Test with size"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses OBJECT-TYPE with range constraint" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX Integer32 (0..100)
MAX-ACCESS read-write
STATUS current
DESCRIPTION "Test with range"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
end
describe "description cleaning" do
@tag :yecc_required
test "parses OBJECT-TYPE with multiline description" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "This is a
multiline
description"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses OBJECT-TYPE with very long description" do
long_desc = String.duplicate("This is a very long description. ", 100)
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "#{long_desc}"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses OBJECT-TYPE with empty description" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION ""
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
end
describe "edge cases" do
@tag :yecc_required
test "handles very large MIB file" do
# Generate large MIB with many OBJECT-TYPE definitions
objects =
Enum.map(1..100, fn i ->
"""
testObject#{i} OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Test object #{i}"
::= { test #{i} }
"""
end)
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
#{Enum.join(objects, "\n")}
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
test "tokenize handles very long identifier" do
long_id = String.duplicate("a", 200)
result = Parser.tokenize(long_id)
assert {:ok, tokens} = result
refute Enum.empty?(tokens)
end
test "tokenize handles many tokens" do
identifiers = Enum.map_join(1..500, " ", fn i -> "id#{i}" end)
result = Parser.tokenize(identifiers)
assert {:ok, tokens} = result
refute Enum.empty?(tokens)
end
@tag :yecc_required
test "parses MIB with deeply nested OIDs" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Test"
::= { iso org dod internet private enterprises test 1 2 3 4 5 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
end
describe "real-world MIB patterns" do
@tag :yecc_required
test "parses SNMPv2-style MIB" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY, OBJECT-TYPE, Integer32
FROM SNMPv2-SMI
MODULE-COMPLIANCE, OBJECT-GROUP
FROM SNMPv2-CONF;
testMIB MODULE-IDENTITY
LAST-UPDATED "202501010000Z"
ORGANIZATION "Test Organization"
CONTACT-INFO "admin@test.com"
DESCRIPTION "Test MIB for unit testing"
REVISION "202501010000Z"
DESCRIPTION "Initial version"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses MIB with table definition" do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testTable OBJECT-TYPE
SYNTAX SEQUENCE OF TestEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "Test table"
::= { test 1 }
testEntry OBJECT-TYPE
SYNTAX TestEntry
MAX-ACCESS not-accessible
STATUS current
DESCRIPTION "Test entry"
INDEX { testIndex }
::= { testTable 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
@tag :yecc_required
test "parses MIB with various access levels" do
access_levels = ["read-only", "read-write", "read-create", "not-accessible", "accessible-for-notify"]
for access <- access_levels do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS #{access}
STATUS current
DESCRIPTION "Test"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
end
@tag :yecc_required
test "parses MIB with various status values" do
statuses = ["current", "deprecated", "obsolete"]
for status <- statuses do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS #{status}
DESCRIPTION "Test"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
end
@tag :yecc_required
test "parses MIB with various syntax types" do
types = [
"Integer32",
"OCTET STRING",
"OBJECT IDENTIFIER",
"Counter32",
"Counter64",
"Gauge32",
"TimeTicks",
"IpAddress"
]
for type <- types do
mib = """
TEST-MIB DEFINITIONS ::= BEGIN
testObject OBJECT-TYPE
SYNTAX #{type}
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Test"
::= { test 1 }
END
"""
result = Parser.parse(mib)
assert is_tuple(result) and elem(result, 0) in [:ok, :error]
end
end
@tag :yecc_required
test "parser has known bug with index 10 (newline character)" do
mib_path = Path.join(:code.priv_dir(:towerops), "mibs/ubnt/UBNT-MIB")
{:ok, content} = File.read(mib_path)
{:ok, parsed} = Parser.parse(content)
definitions = Map.get(parsed, :definitions, [])
# ubntAFLTU is defined as { ubntMIB 10 } in the MIB file
# But the parser returns sub_index: nil because 10 is the newline character
ubnt_afltu = Enum.find(definitions, &(Map.get(&1, :name) == "ubntAFLTU"))
assert %{name: "ubntAFLTU"} = ubnt_afltu
assert Map.get(ubnt_afltu, :parent) == "ubntMIB"
assert Map.get(ubnt_afltu, :sub_index) == nil, "Known parser bug: index 10 not extracted"
end
end
end