defmodule SnmpKit.SnmpLib.MIB.CompilerTest do @moduledoc """ Tests for the MIB Compiler interface. The Compiler module provides a high-level interface to the MIB compilation pipeline, which is implemented via the Parser module using YACC-based parsing. """ use ExUnit.Case, async: true alias SnmpKit.SnmpLib.MIB.Compiler alias SnmpKit.SnmpLib.MIB.Parser # Skip these tests if yecc is not available (requires :parsetools) @moduletag :yecc_required describe "compile_string/2" do test "compiles a minimal MIB successfully" do # A valid v1 MIB requires at least one definition mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert compiled.name == "TEST-MIB" assert compiled.format == :binary assert map_size(compiled.symbols) == 1 assert Map.has_key?(compiled.symbols, "testNode") assert compiled.dependencies == [] end test "compiles a MIB with imports" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN IMPORTS DisplayString FROM SNMPv2-TC enterprises FROM SNMPv2-SMI; testNode OBJECT IDENTIFIER ::= { enterprises 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert compiled.name == "TEST-MIB" assert compiled.dependencies == ["SNMPv2-TC", "SNMPv2-SMI"] end test "compiles a MIB with object definitions" do mib_content = "TEST-MIB DEFINITIONS ::= BEGIN\n" <> "testObject OBJECT-TYPE\n" <> " SYNTAX INTEGER\n" <> " MAX-ACCESS read-only\n" <> " STATUS current\n" <> " DESCRIPTION \"A test object\"\n" <> " ::= { enterprises 12345 1 }\n" <> "END\n" assert {:ok, compiled} = Compiler.compile_string(mib_content) assert compiled.name == "TEST-MIB" assert map_size(compiled.symbols) > 0 assert Map.has_key?(compiled.symbols, "testObject") end test "returns error for invalid MIB syntax" do mib_content = """ INVALID MIB SYNTAX """ assert {:error, errors} = Compiler.compile_string(mib_content) assert is_list(errors) assert errors != [] end test "respects compile options" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content, format: :json) assert compiled.format == :json end end describe "compile/2" do test "compiles a MIB file from disk" do # Use a known good MIB file from fixtures mib_path = "test/fixtures/mibs/working/RFC1213-MIB.mib" if File.exists?(mib_path) do assert {:ok, compiled} = Compiler.compile(mib_path) assert compiled.name == "RFC1213-MIB" assert compiled.format == :binary else # Skip if fixtures aren't available :ok end end test "returns error for non-existent file" do assert {:error, errors} = Compiler.compile("non_existent.mib") assert is_list(errors) [error | _] = errors assert error.type == :file_not_found end end describe "integration with Parser" do test "Compiler and Parser produce compatible results" do mib_content = "TEST-MIB DEFINITIONS ::= BEGIN\n" <> "IMPORTS\n" <> " DisplayString FROM SNMPv2-TC;\n" <> "\n" <> "testObject OBJECT-TYPE\n" <> " SYNTAX DisplayString\n" <> " MAX-ACCESS read-only\n" <> " STATUS current\n" <> " DESCRIPTION \"Test object\"\n" <> " ::= { enterprises 12345 1 }\n" <> "END\n" # Test that Compiler properly wraps Parser functionality assert {:ok, compiled} = Compiler.compile_string(mib_content) assert {:ok, parsed} = Parser.parse(mib_content) # Verify the compiler adds the expected structure assert compiled.name == parsed.name assert length(compiled.dependencies) == length(parsed.imports) assert map_size(compiled.symbols) == length(parsed.definitions) end end describe "compile_all/2" do test "compiles multiple MIBs successfully" do # Use minimal valid MIBs mib1 = """ MIB-ONE DEFINITIONS ::= BEGIN nodeOne OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 1 } END """ mib2 = """ MIB-TWO DEFINITIONS ::= BEGIN nodeTwo OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 2 } END """ # Write temp files dir = Path.join(System.tmp_dir!(), "test_mibs_#{:rand.uniform(1_000_000)}") File.mkdir_p!(dir) file1 = Path.join(dir, "mib-one.mib") file2 = Path.join(dir, "mib-two.mib") File.write!(file1, mib1) File.write!(file2, mib2) assert {:ok, compiled_list} = Compiler.compile_all([file1, file2]) assert length(compiled_list) == 2 assert Enum.any?(compiled_list, &(&1.name == "MIB-ONE")) assert Enum.any?(compiled_list, &(&1.name == "MIB-TWO")) # Cleanup File.rm_rf!(dir) end test "returns error when any MIB fails to compile" do valid_mib = """ VALID-MIB DEFINITIONS ::= BEGIN validNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 1 } END """ invalid_mib = "INVALID SYNTAX" dir = Path.join(System.tmp_dir!(), "test_mibs_#{:rand.uniform(1_000_000)}") File.mkdir_p!(dir) file1 = Path.join(dir, "valid.mib") file2 = Path.join(dir, "invalid.mib") File.write!(file1, valid_mib) File.write!(file2, invalid_mib) assert {:error, errors} = Compiler.compile_all([file1, file2]) assert is_list(errors) refute Enum.empty?(errors) # Cleanup File.rm_rf!(dir) end test "handles empty file list" do assert {:ok, []} = Compiler.compile_all([]) end test "continues compiling after encountering errors" do # Test that compile_all doesn't stop on first error dir = Path.join(System.tmp_dir!(), "test_mibs_#{:rand.uniform(1_000_000)}") File.mkdir_p!(dir) mib1 = """ MIB-ONE DEFINITIONS ::= BEGIN nodeOne OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 1 } END """ file1 = Path.join(dir, "valid1.mib") file2 = Path.join(dir, "invalid.mib") file3 = Path.join(dir, "valid2.mib") File.write!(file1, mib1) File.write!(file2, "INVALID") File.write!(file3, mib1) assert {:error, errors} = Compiler.compile_all([file1, file2, file3]) # Should have one error (file2) assert length(errors) == 1 [{error_file, _error_list}] = errors assert error_file == file2 File.rm_rf!(dir) end end describe "load_compiled/1" do test "loads a previously compiled MIB" do # Create a compiled MIB mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ {:ok, compiled} = Compiler.compile_string(mib_content) # Save it to disk dir = Path.join(System.tmp_dir!(), "test_compiled_#{:rand.uniform(1_000_000)}") File.mkdir_p!(dir) path = Path.join(dir, "test.mib") compiled_with_type = Map.put(compiled, :__type__, :compiled_mib) File.write!(path, :erlang.term_to_binary(compiled_with_type)) # Load it back assert {:ok, loaded} = Compiler.load_compiled(path) assert loaded.name == compiled.name assert loaded.__type__ == :compiled_mib File.rm_rf!(dir) end test "returns error for non-existent file" do assert {:error, :enoent} = Compiler.load_compiled("non_existent.mib") end test "returns error for invalid compiled format" do dir = Path.join(System.tmp_dir!(), "test_compiled_#{:rand.uniform(1_000_000)}") File.mkdir_p!(dir) path = Path.join(dir, "invalid.mib") # Write invalid data (not a compiled MIB) File.write!(path, :erlang.term_to_binary(%{random: "data"})) assert {:error, :invalid_compiled_format} = Compiler.load_compiled(path) File.rm_rf!(dir) end end describe "compile options" do test "validates option with validate: true" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, _compiled} = Compiler.compile_string(mib_content, validate: true) end test "skips validation with validate: false" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, _compiled} = Compiler.compile_string(mib_content, validate: false) end test "accepts format option" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content, format: :erlang) assert compiled.format == :erlang end test "accepts output_dir option" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, _compiled} = Compiler.compile_string(mib_content, output_dir: "/tmp/custom") end test "accepts optimize option" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, _compiled} = Compiler.compile_string(mib_content, optimize: false) end test "accepts include_paths option" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, _compiled} = Compiler.compile_string(mib_content, include_paths: ["/tmp/mibs"]) end test "accepts warnings_as_errors option" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, _compiled} = Compiler.compile_string(mib_content, warnings_as_errors: true) end test "merges options with defaults" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ # Only specify format, should merge with defaults assert {:ok, compiled} = Compiler.compile_string(mib_content, format: :json) assert compiled.format == :json end end describe "error handling" do test "handles parser syntax errors gracefully" do mib_content = "INVALID SYNTAX" assert {:error, errors} = Compiler.compile_string(mib_content) assert is_list(errors) refute Enum.empty?(errors) end test "handles missing file errors" do assert {:error, errors} = Compiler.compile("non_existent.mib") [error | _] = errors assert error.type == :file_not_found assert String.contains?(error.message, "Could not read MIB file") end test "handles parse errors with line numbers" do # Incomplete MIB that should fail parsing mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN """ assert {:error, errors} = Compiler.compile_string(mib_content) assert is_list(errors) end end describe "symbol table construction" do test "builds symbol table from definitions" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN nodeOne OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 1 } nodeTwo OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 2 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert map_size(compiled.symbols) == 2 assert Map.has_key?(compiled.symbols, "nodeOne") assert Map.has_key?(compiled.symbols, "nodeTwo") end test "symbol table contains definition data" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert Map.has_key?(compiled.symbols, "testNode") symbol = compiled.symbols["testNode"] assert symbol.name == "testNode" end end describe "dependency extraction" do test "extracts no dependencies from MIB without imports" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert compiled.dependencies == [] end test "extracts single dependency" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN IMPORTS DisplayString FROM SNMPv2-TC; testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert compiled.dependencies == ["SNMPv2-TC"] end test "extracts multiple dependencies" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN IMPORTS DisplayString FROM SNMPv2-TC enterprises FROM SNMPv2-SMI; testNode OBJECT IDENTIFIER ::= { enterprises 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert "SNMPv2-TC" in compiled.dependencies assert "SNMPv2-SMI" in compiled.dependencies assert length(compiled.dependencies) == 2 end test "deduplicates dependencies" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN IMPORTS DisplayString, ObjectName FROM SNMPv2-TC enterprises FROM SNMPv2-SMI; testNode OBJECT IDENTIFIER ::= { enterprises 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert length(compiled.dependencies) == 2 end end describe "compiled MIB structure" do test "includes all required fields" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert Map.has_key?(compiled, :name) assert Map.has_key?(compiled, :version) assert Map.has_key?(compiled, :format) assert Map.has_key?(compiled, :path) assert Map.has_key?(compiled, :metadata) assert Map.has_key?(compiled, :oid_tree) assert Map.has_key?(compiled, :symbols) assert Map.has_key?(compiled, :dependencies) end test "sets path to nil for string compilation" do mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testNode OBJECT IDENTIFIER ::= { 1 3 6 1 4 1 99999 } END """ assert {:ok, compiled} = Compiler.compile_string(mib_content) assert compiled.path == nil end end end