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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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