516 lines
15 KiB
Elixir
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
|