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

530 lines
15 KiB
Elixir

defmodule SnmpKit.SnmpLib.MIB.SnmpTokenizerTest do
use ExUnit.Case, async: true
alias SnmpKit.SnmpLib.MIB.SnmpTokenizer
describe "tokenize/2" do
test "tokenizes empty input" do
assert {:ok, tokens} = SnmpTokenizer.tokenize(~c"", &SnmpTokenizer.null_get_line/0)
assert [{:"$end", 1}] = tokens
end
test "tokenizes simple MIB header" do
input = ~c"TEST-MIB DEFINITIONS ::= BEGIN END"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
# TEST-MIB is tokenized as :variable (uppercase identifier)
assert :variable in token_types
assert :DEFINITIONS in token_types
assert :BEGIN in token_types
assert :END in token_types
end
test "tokenizes reserved words" do
reserved_words = [
"DEFINITIONS",
"BEGIN",
"END",
"IMPORTS",
"FROM",
"OBJECT-TYPE",
"SYNTAX",
"MAX-ACCESS",
"STATUS",
"DESCRIPTION"
]
for word <- reserved_words do
input = String.to_charlist(word)
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# First token should be the reserved word
refute Enum.empty?(tokens)
end
end
test "tokenizes identifiers" do
input = ~c"testObject myIdentifier test123"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
atoms = Enum.filter(tokens, &match?({:atom, _, _}, &1))
assert length(atoms) == 3
end
test "tokenizes numbers" do
input = ~c"0 123 456789"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
numbers = Enum.filter(tokens, &match?({:integer, _, _}, &1))
assert length(numbers) == 3
end
test "tokenizes negative numbers" do
input = ~c"-1 -123"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Might be tokenized as minus sign + integer, depends on implementation
assert length(tokens) >= 2
end
test "tokenizes strings with quotes" do
input = ~c"\"test string\" \"another\""
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Strings might be tokenized as :string or just handled as tokens
# Just verify tokenization succeeds
refute Enum.empty?(tokens)
end
test "tokenizes special characters" do
input = ~c"::= { } ( ) , ; ."
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Should have various special character tokens
assert length(tokens) > 5
end
test "tokenizes OID notation" do
input = ~c"{ test 1 2 3 }"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Should have braces, identifier, and numbers
assert length(tokens) >= 6
end
test "tokenizes complete OBJECT-TYPE definition" do
input = ~c"""
testObject OBJECT-TYPE
SYNTAX Integer32
MAX-ACCESS read-only
STATUS current
DESCRIPTION "Test object"
::= { test 1 }
"""
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
assert :"OBJECT-TYPE" in token_types
assert :SYNTAX in token_types
assert :"MAX-ACCESS" in token_types
assert :STATUS in token_types
assert :DESCRIPTION in token_types
end
test "tokenizes access modifiers" do
access_values = ["read-only", "read-write", "not-accessible", "read-create"]
for access <- access_values do
input = String.to_charlist(access)
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
refute Enum.empty?(tokens)
end
end
test "tokenizes SMI types" do
types = ["INTEGER", "OCTET STRING", "Counter32", "Gauge32", "TimeTicks", "IpAddress"]
for type <- types do
input = String.to_charlist(type)
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
refute Enum.empty?(tokens)
end
end
test "tokenizes hyphenated keywords" do
keywords = [
"OBJECT-TYPE",
"MAX-ACCESS",
"MODULE-IDENTITY",
"OBJECT-IDENTITY",
"TEXTUAL-CONVENTION"
]
for keyword <- keywords do
input = String.to_charlist(keyword)
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Should tokenize as single keyword token
non_end_tokens = Enum.reject(tokens, &match?({:"$end", _}, &1))
assert length(non_end_tokens) == 1
end
end
test "handles whitespace correctly" do
input = ~c" test 123 \n OBJECT-TYPE "
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Whitespace should be ignored, only meaningful tokens returned
non_end_tokens = Enum.reject(tokens, &match?({:"$end", _}, &1))
assert length(non_end_tokens) == 3
end
test "handles comments" do
input = ~c"test -- this is a comment\nOBJECT-TYPE"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Comments should be ignored
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
assert :atom in token_types
assert :"OBJECT-TYPE" in token_types
end
test "handles multiline input" do
input = ~c"""
TEST-MIB
DEFINITIONS
::=
BEGIN
END
"""
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
# TEST-MIB is tokenized as :variable (uppercase identifier)
assert :variable in token_types
assert :DEFINITIONS in token_types
assert :BEGIN in token_types
assert :END in token_types
end
test "tokenizes with line numbers" do
input = ~c"test\nDEFINITIONS\nBEGIN"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Tokens should have different line numbers
lines =
Enum.map(tokens, fn
{_type, line} -> line
{_type, _value, line} -> line
end)
assert lines |> Enum.uniq() |> length() > 1
end
test "preserves token order" do
input = ~c"first second third"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
atoms = Enum.filter(tokens, &match?({:atom, _, _}, &1))
# Should be in order
assert length(atoms) == 3
end
test "handles complex MIB structure" do
input = ~c"""
TEST-MIB DEFINITIONS ::= BEGIN
IMPORTS
MODULE-IDENTITY FROM SNMPv2-SMI;
testMIB MODULE-IDENTITY
LAST-UPDATED "202501010000Z"
ORGANIZATION "Test Org"
::= { test 1 }
END
"""
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
assert :DEFINITIONS in token_types
assert :BEGIN in token_types
assert :IMPORTS in token_types
assert :"MODULE-IDENTITY" in token_types
assert :FROM in token_types
assert :END in token_types
end
test "tokenizes SIZE constraint" do
input = ~c"SIZE (0..255)"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
assert :SIZE in token_types
end
test "tokenizes SEQUENCE OF" do
input = ~c"SEQUENCE OF TestEntry"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
assert :SEQUENCE in token_types
assert :OF in token_types
end
end
describe "format_error/1" do
test "formats illegal character error" do
error = {:illegal, ?@}
formatted = SnmpTokenizer.format_error(error)
formatted_str = List.to_string(formatted)
assert formatted_str =~ "illegal character"
end
test "formats unterminated string error" do
error = {:unterminated_string, 42}
formatted = SnmpTokenizer.format_error(error)
formatted_str = List.to_string(formatted)
assert formatted_str =~ "unterminated string"
assert formatted_str =~ "42"
end
test "formats unterminated quote error" do
error = {:unterminated_quote, 10}
formatted = SnmpTokenizer.format_error(error)
formatted_str = List.to_string(formatted)
assert formatted_str =~ "unterminated quote"
assert formatted_str =~ "10"
end
test "formats unknown error" do
error = {:unknown_error, "details"}
formatted = SnmpTokenizer.format_error(error)
refute Enum.empty?(formatted)
end
test "formats atom error" do
error = :some_error
formatted = SnmpTokenizer.format_error(error)
refute Enum.empty?(formatted)
end
end
describe "null_get_line/0" do
test "returns eof" do
assert SnmpTokenizer.null_get_line() == :eof
end
end
describe "test/0" do
test "runs test function without crashing" do
# The test/0 function tokenizes a test string and prints output
# We just verify it doesn't crash
assert SnmpTokenizer.test() in [:ok, :error]
end
end
describe "edge cases" do
test "handles very long identifiers" do
# Use a more reasonable length to avoid atom table limits (255 bytes max for atoms)
long_id = "a" |> String.duplicate(200) |> String.to_charlist()
assert {:ok, tokens} = SnmpTokenizer.tokenize(long_id, &SnmpTokenizer.null_get_line/0)
refute Enum.empty?(tokens)
end
test "handles many tokens" do
# Create input with many identifiers
identifiers = Enum.map_join(1..100, " ", fn i -> "id#{i}" end)
input = String.to_charlist(identifiers)
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
atoms = Enum.filter(tokens, &match?({:atom, _, _}, &1))
assert length(atoms) == 100
end
test "handles deeply nested braces" do
input = ~c"{ { { test } } }"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
assert length(tokens) > 6
end
test "handles mixed case keywords" do
# Reserved words should be case-sensitive
input = ~c"begin BEGIN"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
# Only "BEGIN" should be recognized as keyword
begin_count = Enum.count(token_types, &(&1 == :BEGIN))
assert begin_count == 1
# "begin" should be an atom (lowercase identifier)
atom_count = Enum.count(tokens, &match?({:atom, _, _}, &1))
assert atom_count >= 1
end
test "handles unicode characters in strings" do
# Basic ASCII string should work
input = ~c"\"test\""
assert {:ok, _tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
end
test "handles empty strings" do
input = ~c"\"\""
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
refute Enum.empty?(tokens)
end
test "handles consecutive operators" do
input = ~c"::=::="
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Should tokenize as two ::= operators or similar
assert length(tokens) >= 2
end
test "handles tabs and mixed whitespace" do
input = ~c"test\t\tOBJECT-TYPE\n\n BEGIN"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
assert :atom in token_types
assert :"OBJECT-TYPE" in token_types
assert :BEGIN in token_types
end
test "handles numbers with leading zeros" do
input = ~c"001 0123"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
integers = Enum.filter(tokens, &match?({:integer, _, _}, &1))
assert length(integers) == 2
end
test "handles very large numbers" do
input = ~c"999999999999"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
integers = Enum.filter(tokens, &match?({:integer, _, _}, &1))
assert length(integers) == 1
end
end
describe "reserved word coverage" do
test "tokenizes all common reserved words" do
# Test a sampling of reserved words to ensure coverage
words = [
"DEFINITIONS",
"BEGIN",
"END",
"IMPORTS",
"FROM",
"EXPORTS",
"OBJECT",
"IDENTIFIER",
"OBJECT-TYPE",
"SYNTAX",
"ACCESS",
"MAX-ACCESS",
"STATUS",
"DESCRIPTION",
"REFERENCE",
"INDEX",
"SEQUENCE",
"OF",
"SIZE",
"INTEGER",
"OCTET",
"STRING",
"Counter32",
"Gauge32",
"MODULE-IDENTITY",
"TEXTUAL-CONVENTION",
"OBJECT-GROUP",
"NOTIFICATION-TYPE"
]
for word <- words do
input = String.to_charlist(word)
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
# Should successfully tokenize without error
refute Enum.empty?(tokens)
end
end
test "distinguishes reserved words from similar identifiers" do
# "OBJECT" is reserved, "object" is lowercase atom
input = ~c"OBJECT object"
assert {:ok, tokens} = SnmpTokenizer.tokenize(input, &SnmpTokenizer.null_get_line/0)
token_types =
Enum.map(tokens, fn
{type, _line} -> type
{type, _value, _line} -> type
end)
# OBJECT should be keyword
assert :OBJECT in token_types
# "object" should be an atom (lowercase identifier)
atoms = Enum.filter(tokens, &match?({:atom, _, _}, &1))
refute Enum.empty?(atoms)
end
end
end