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

516 lines
15 KiB
Elixir

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
doctest Compiler, except: [compile: 1, compile_string: 1, compile_string: 2, load_compiled: 1, compile_all: 1]
# 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 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")
[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])
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)
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)
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