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