diff --git a/test/snmpkit/snmp_lib/mib/error_test.exs b/test/snmpkit/snmp_lib/mib/error_test.exs new file mode 100644 index 00000000..6cf3d30f --- /dev/null +++ b/test/snmpkit/snmp_lib/mib/error_test.exs @@ -0,0 +1,438 @@ +defmodule SnmpKit.SnmpLib.MIB.ErrorTest do + use ExUnit.Case, async: true + + alias SnmpKit.SnmpLib.MIB.Error + + describe "new/2" do + test "creates error with type and message" do + error = Error.new(:syntax_error) + + assert error.type == :syntax_error + assert error.message == "Syntax error" + assert error.line == nil + assert error.column == nil + assert error.context == %{} + assert error.suggestions == [] + end + + test "creates error with line and column" do + error = Error.new(:semantic_error, line: 42, column: 10) + + assert error.type == :semantic_error + assert error.line == 42 + assert error.column == 10 + end + + test "creates error with context" do + context = %{file: "test.mib", token: "ACCESS"} + error = Error.new(:type_error, context: context) + + assert error.context == context + end + + test "generates message for unexpected_token error" do + error = Error.new(:unexpected_token, expected: :max_access, actual: :access) + + assert error.message =~ "Expected" + assert error.message =~ "max_access" + assert error.message =~ "access" + end + + test "generates message for unexpected_token with value" do + error = Error.new(:unexpected_token, expected: :semicolon, actual: :identifier, value: "test") + + assert error.message =~ "test" + end + + test "generates message for unexpected_eof" do + error = Error.new(:unexpected_eof, expected: "closing brace") + + assert error.message == "Unexpected end of file, expected closing brace" + end + + test "generates message for unterminated_string" do + error = Error.new(:unterminated_string) + + assert error.message == "Unterminated string literal" + end + + test "generates message for invalid_number" do + error = Error.new(:invalid_number, value: "12.34.56") + + assert error.message == "Invalid number format: '12.34.56'" + end + + test "generates message for invalid_identifier" do + error = Error.new(:invalid_identifier, value: "1invalid") + + assert error.message == "Invalid identifier: '1invalid'" + end + + test "generates message for file_not_found" do + error = Error.new(:file_not_found, file: "/path/to/missing.mib") + + assert error.message == "File not found: /path/to/missing.mib" + end + + test "generates message for import_error" do + error = Error.new(:import_error, symbol: "sysDescr", from_module: "SNMPv2-MIB") + + assert error.message == "Cannot import 'sysDescr' from 'SNMPv2-MIB'" + end + + test "generates message for duplicate_definition" do + error = Error.new(:duplicate_definition, name: "testObject") + + assert error.message == "Duplicate definition of 'testObject'" + end + + test "generates generic message for unknown error type" do + error = Error.new(:constraint_error) + + assert error.message == "Constraint error" + end + + test "generates suggestions for unexpected_token MAX-ACCESS" do + error = Error.new(:unexpected_token, expected: :max_access, actual: :access) + + assert length(error.suggestions) > 0 + assert Enum.any?(error.suggestions, &String.contains?(&1, "MAX-ACCESS")) + end + + test "generates suggestions for unexpected_token mandatory/current" do + error = Error.new(:unexpected_token, expected: :current, actual: :mandatory) + + assert length(error.suggestions) > 0 + assert Enum.any?(error.suggestions, &String.contains?(&1, "deprecated")) + end + + test "generates suggestions for unexpected_token OBJECT-TYPE" do + error = Error.new(:unexpected_token, expected: :object_type, actual: :object_identity) + + assert length(error.suggestions) > 0 + assert Enum.any?(error.suggestions, &String.contains?(&1, "OBJECT-TYPE")) + end + + test "generates suggestions for unterminated_string" do + error = Error.new(:unterminated_string) + + assert length(error.suggestions) > 0 + assert Enum.any?(error.suggestions, &String.contains?(&1, "quote")) + end + + test "generates suggestions for file_not_found with RFC file" do + error = Error.new(:file_not_found, file: "RFC1213-MIB.txt") + + assert length(error.suggestions) > 0 + assert Enum.any?(error.suggestions, &String.contains?(&1, "naming conventions")) + end + + test "generates suggestions for file_not_found with MIB file" do + error = Error.new(:file_not_found, file: "TEST-MIB.mib") + + assert length(error.suggestions) > 0 + assert Enum.any?(error.suggestions, &String.contains?(&1, "extension")) + end + + test "generates suggestions for file_not_found without RFC/MIB" do + error = Error.new(:file_not_found, file: "/random/file.txt") + + assert length(error.suggestions) == 1 + assert List.first(error.suggestions) =~ "file path is correct" + end + + test "generates suggestions for import_error" do + error = Error.new(:import_error, symbol: "test", from_module: "TEST-MIB") + + assert length(error.suggestions) > 0 + assert Enum.any?(error.suggestions, &String.contains?(&1, "import")) + end + + test "generates no suggestions for unknown error types" do + error = Error.new(:semantic_error) + + assert error.suggestions == [] + end + end + + describe "format/2" do + test "formats error without location" do + error = Error.new(:syntax_error) + formatted = Error.format(error) + + assert formatted =~ "Syntax error" + end + + test "formats error with line only" do + error = Error.new(:type_error, line: 42) + formatted = Error.format(error) + + assert formatted =~ "Line 42" + end + + test "formats error with column only" do + error = Error.new(:semantic_error, column: 10) + formatted = Error.format(error) + + assert formatted =~ "Column 10" + end + + test "formats error with line and column" do + error = Error.new(:unexpected_token, expected: :semicolon, actual: :identifier, line: 42, column: 10) + formatted = Error.format(error) + + assert formatted =~ "Line 42, column 10" + assert formatted =~ "Expected" + end + + test "formats error with suggestions" do + error = Error.new(:unterminated_string, line: 5) + formatted = Error.format(error) + + assert formatted =~ "Suggestions:" + assert formatted =~ "quote" + end + + test "formats error without suggestions" do + error = Error.new(:semantic_error, line: 10) + formatted = Error.format(error) + + refute formatted =~ "Suggestions:" + end + + test "formats multiple suggestions" do + error = Error.new(:import_error, symbol: "test", from_module: "TEST") + formatted = Error.format(error) + + assert formatted =~ "Suggestions:" + # Count bullets + bullet_count = formatted |> String.split("•") |> length() |> Kernel.-(1) + assert bullet_count == length(error.suggestions) + end + + test "accepts format options" do + error = Error.new(:syntax_error, line: 1) + + # Should not crash with options + assert is_binary(Error.format(error, color: true)) + assert is_binary(Error.format(error, verbose: true)) + assert is_binary(Error.format(error, [])) end + + test "formats unexpected_token error with custom message" do + error = Error.new(:unexpected_token, message: "Custom error message", line: 5) + formatted = Error.format(error) + + assert formatted =~ "Custom error message" + assert formatted =~ "Line 5" + end + + test "formats unexpected_token with nil values" do + error = Error.new(:unexpected_token, expected: nil, actual: nil) + formatted = Error.format(error) + + assert is_binary(formatted) + assert formatted =~ "unknown" + end + + test "formats unexpected_token with empty string values" do + error = Error.new(:unexpected_token, expected: "", actual: "") + formatted = Error.format(error) + + assert is_binary(formatted) + assert formatted =~ "unknown" + end + + test "formats unexpected_token with end of input" do + error = Error.new(:unexpected_token, expected: :semicolon, actual: "") + formatted = Error.format(error) + + # Empty string actual is converted to "unknown" by format_token_value + assert formatted =~ "unknown" + end + + test "formats unexpected_token with nil actual" do + error = Error.new(:unexpected_token, expected: :identifier, actual: nil) + formatted = Error.format(error) + + # nil actual is converted to "unknown" by format_token_value + assert formatted =~ "unknown" + end + + test "formats unexpected_token with various atom values" do + error = Error.new(:unexpected_token, expected: :keyword, actual: :identifier) + formatted = Error.format(error) + + assert formatted =~ "keyword" + assert formatted =~ "identifier" + end + + test "formats unexpected_token with binary values" do + error = Error.new(:unexpected_token, expected: "semicolon", actual: "comma") + formatted = Error.format(error) + + assert formatted =~ "semicolon" + assert formatted =~ "comma" + end + + test "formats unexpected_token with mixed value types" do + error = Error.new(:unexpected_token, expected: :keyword, actual: "string") + formatted = Error.format(error) + + assert formatted =~ "keyword" + assert formatted =~ "string" + end + + test "formats unexpected_token with non-standard values" do + error = Error.new(:unexpected_token, expected: 123, actual: [:list]) + formatted = Error.format(error) + + # Should use inspect for non-atom, non-binary values + assert is_binary(formatted) + end + end + + describe "error struct" do + test "has correct structure" do + error = %Error{ + type: :syntax_error, + message: "test", + line: 1, + column: 2, + context: %{key: "value"}, + suggestions: ["suggestion"] + } + + assert error.type == :syntax_error + assert error.message == "test" + assert error.line == 1 + assert error.column == 2 + assert error.context == %{key: "value"} + assert error.suggestions == ["suggestion"] + end + + test "suggestions default to empty list" do + error = %Error{type: :test, message: "test"} + + assert error.suggestions == [] + end + end + + describe "edge cases" do + test "handles nil options gracefully" do + error = Error.new(:syntax_error, nil) + + assert error.type == :syntax_error + assert is_binary(error.message) + end + + test "handles empty options list" do + error = Error.new(:semantic_error, []) + + assert error.type == :semantic_error + assert error.context == %{} + end + + test "handles error with all fields nil" do + error = Error.new(:type_error, line: nil, column: nil, context: nil) + + formatted = Error.format(error) + assert is_binary(formatted) + end + + test "generates messages for all supported error types" do + error_types = [ + :syntax_error, + :semantic_error, + :import_error, + :type_error, + :constraint_error, + :duplicate_definition, + :file_not_found, + :unterminated_string, + :unexpected_token, + :unexpected_eof, + :invalid_number, + :invalid_identifier + ] + + for type <- error_types do + error = Error.new(type) + assert is_binary(error.message) + assert byte_size(error.message) > 0 + end + end + + test "handles context with various data types" do + contexts = [ + %{string: "value"}, + %{integer: 42}, + %{atom: :test}, + %{list: [1, 2, 3]}, + %{nested: %{key: "value"}}, + %{} + ] + + for context <- contexts do + error = Error.new(:test_error, context: context) + assert error.context == context + end + end + + test "handles line and column edge values" do + # Large line numbers + error1 = Error.new(:test, line: 999_999) + assert Error.format(error1) =~ "999999" + + # Zero line/column + error2 = Error.new(:test, line: 0, column: 0) + formatted2 = Error.format(error2) + assert formatted2 =~ "Line 0" + assert formatted2 =~ "column 0" + + # Negative values (shouldn't happen but should handle) + error3 = Error.new(:test, line: -1, column: -5) + assert is_binary(Error.format(error3)) + end + end + + describe "message generation variations" do + test "unexpected_token with only expected" do + error = Error.new(:unexpected_token, expected: :semicolon) + assert error.message =~ "semicolon" + end + + test "unexpected_token with only actual" do + error = Error.new(:unexpected_token, actual: :comma) + assert error.message =~ "comma" + end + + test "unexpected_token with all fields" do + error = Error.new(:unexpected_token, + expected: :semicolon, + actual: :comma, + value: ",", + line: 10, + column: 5 + ) + + assert error.message =~ "semicolon" + assert error.message =~ "comma" + assert error.message =~ "," + end + + test "file_not_found without file option" do + error = Error.new(:file_not_found) + assert is_binary(error.message) + end + + test "import_error without options" do + error = Error.new(:import_error) + assert is_binary(error.message) + end + + test "duplicate_definition without name" do + error = Error.new(:duplicate_definition) + assert is_binary(error.message) + end + end +end