Skip to main content
Glama
test_error_formatter.py24.5 kB
"""Unit tests for error formatting functionality.""" import re from datetime import datetime from pathlib import Path from src.autodoc_mcp.core.error_formatter import ( ErrorFormatter, ErrorSeverity, FormattedError, ResponseFormatter, ) from src.autodoc_mcp.exceptions import ( NetworkError, PackageNotFoundError, ProjectParsingError, ) class TestFormattedError: """Test FormattedError dataclass.""" def test_formatted_error_creation(self): """Test basic FormattedError creation.""" error = FormattedError( message="Test error message", suggestion="Test suggestion", severity=ErrorSeverity.ERROR, error_code="test_error", context={"key": "value"}, recoverable=True, ) assert error.message == "Test error message" assert error.suggestion == "Test suggestion" assert error.severity == ErrorSeverity.ERROR assert error.error_code == "test_error" assert error.context == {"key": "value"} assert error.recoverable is True def test_formatted_error_defaults(self): """Test FormattedError with default values.""" error = FormattedError(message="Test message") assert error.message == "Test message" assert error.suggestion is None assert error.severity == ErrorSeverity.ERROR assert error.error_code is None assert error.context is None assert error.recoverable is True class TestErrorSeverity: """Test ErrorSeverity enum.""" def test_severity_values(self): """Test all severity level values.""" assert ErrorSeverity.INFO.value == "info" assert ErrorSeverity.WARNING.value == "warning" assert ErrorSeverity.ERROR.value == "error" assert ErrorSeverity.CRITICAL.value == "critical" class TestErrorFormatter: """Test ErrorFormatter functionality.""" def test_dependency_patterns_defined(self): """Test that dependency patterns are properly defined.""" patterns = ErrorFormatter.DEPENDENCY_PATTERNS assert len(patterns) > 0 # Check pattern structure for pattern, error_type in patterns: assert isinstance(pattern, str) assert isinstance(error_type, str) # Ensure patterns compile re.compile(pattern) class TestDependencyParsingErrors: """Test dependency parsing error formatting.""" def test_format_dependency_parsing_errors_empty_list(self): """Test formatting empty list of dependency errors.""" result = ErrorFormatter.format_dependency_parsing_errors([]) assert result == [] def test_format_dependency_parsing_errors_single_failure(self): """Test formatting single dependency failure.""" failed_deps = [ { "dependency_string": "invalid-pkg-name!@#", "error": "Invalid package name format: 'invalid-pkg-name!@#'", "source": "dependencies", } ] result = ErrorFormatter.format_dependency_parsing_errors(failed_deps) assert len(result) == 1 assert isinstance(result[0], FormattedError) assert result[0].error_code == "invalid_package_name" assert "invalid-pkg-name" in result[0].message def test_format_dependency_parsing_errors_multiple_failures(self): """Test formatting multiple dependency failures.""" failed_deps = [ { "dependency_string": "pkg[unclosed-bracket", "error": "Unclosed bracket in extras: 'pkg[unclosed-bracket'", "source": "dev-dependencies", }, { "dependency_string": "", "error": "Empty dependency string", "source": "dependencies", }, ] result = ErrorFormatter.format_dependency_parsing_errors(failed_deps) assert len(result) == 2 assert result[0].error_code == "malformed_extras" assert result[1].error_code == "empty_dependency" def test_format_single_dependency_error_invalid_package_name(self): """Test formatting invalid package name error.""" result = ErrorFormatter._format_single_dependency_error( "bad@pkg", "Invalid package name format: 'bad@pkg'", "dependencies" ) assert result.error_code == "invalid_package_name" assert "bad@pkg" in result.message assert "dependencies" in result.message assert result.severity == ErrorSeverity.WARNING assert result.recoverable is True assert "bad-pkg" in result.suggestion # Should suggest fix def test_format_single_dependency_error_malformed_extras(self): """Test formatting malformed extras error.""" result = ErrorFormatter._format_single_dependency_error( "pkg[extra", "Unclosed bracket in extras: 'pkg[extra'", "dev-dependencies" ) assert result.error_code == "malformed_extras" assert "pkg[extra" in result.message assert "dev-dependencies" in result.message assert result.severity == ErrorSeverity.WARNING assert "square brackets" in result.suggestion def test_format_single_dependency_error_invalid_version(self): """Test formatting invalid version error.""" result = ErrorFormatter._format_single_dependency_error( "pkg>>1.0", "Invalid version specifier.*'pkg>>1.0'", "dependencies" ) assert result.error_code == "invalid_version" assert "pkg>>1.0" in result.message assert result.severity == ErrorSeverity.WARNING assert "valid version operators" in result.suggestion def test_format_single_dependency_error_empty_dependency(self): """Test formatting empty dependency error.""" result = ErrorFormatter._format_single_dependency_error( "", "Empty dependency string", "dependencies" ) assert result.error_code == "empty_dependency" assert "Empty dependency" in result.message assert "dependencies" in result.message assert result.severity == ErrorSeverity.WARNING def test_format_single_dependency_error_unknown_pattern(self): """Test formatting unknown dependency error pattern.""" result = ErrorFormatter._format_single_dependency_error( "unknown-issue", "Some unknown error message", "dependencies" ) assert result.error_code == "dependency_parse_unknown" assert "unknown-issue" in result.message assert "dependencies" in result.message assert result.severity == ErrorSeverity.WARNING assert result.recoverable is True def test_create_dependency_error_by_type_invalid_package_name(self): """Test creating invalid package name error.""" match = re.search( r"Invalid package name format: '([^']+)'", "Invalid package name format: 'bad@name'", ) result = ErrorFormatter._create_dependency_error_by_type( "invalid_package_name", "bad@name", "dependencies", match ) assert result.error_code == "invalid_package_name" assert result.context["original_name"] == "bad@name" assert result.context["source"] == "dependencies" def test_create_dependency_error_by_type_malformed_extras(self, mocker): """Test creating malformed extras error.""" match = mocker.Mock() match.groups.return_value = () result = ErrorFormatter._create_dependency_error_by_type( "malformed_extras", "pkg[extra", "dev-dependencies", match ) assert result.error_code == "malformed_extras" assert result.context["original_dep"] == "pkg[extra" assert result.context["source"] == "dev-dependencies" def test_create_dependency_error_by_type_fallback(self, mocker): """Test creating error with unknown type falls back gracefully.""" match = mocker.Mock() result = ErrorFormatter._create_dependency_error_by_type( "unknown_type", "test-dep", "dependencies", match ) assert result.error_code == "dependency_parse_unknown" assert "test-dep" in result.message class TestSuggestionGeneration: """Test suggestion generation methods.""" def test_suggest_package_name_fix_basic(self): """Test basic package name fix suggestions.""" result = ErrorFormatter._suggest_package_name_fix("bad@name!") assert result == "bad-name" def test_suggest_package_name_fix_multiple_hyphens(self): """Test package name fix with multiple consecutive special chars.""" result = ErrorFormatter._suggest_package_name_fix("bad---name@@@") assert result == "bad-name" def test_suggest_package_name_fix_leading_trailing_hyphens(self): """Test package name fix removes leading/trailing hyphens.""" result = ErrorFormatter._suggest_package_name_fix("-bad-name-") assert result == "bad-name" def test_suggest_package_name_fix_empty_result(self): """Test package name fix fallback for empty result.""" result = ErrorFormatter._suggest_package_name_fix("@@@") assert result == "package-name" def test_suggest_extras_fix_valid_package(self): """Test extras fix suggestion with valid package name.""" result = ErrorFormatter._suggest_extras_fix("requests[unclosed") assert result == "requests[extra1,extra2]>=1.0.0" def test_suggest_extras_fix_no_package_match(self): """Test extras fix suggestion with no package match.""" result = ErrorFormatter._suggest_extras_fix("[@#$") assert result == "package[extra1,extra2]>=1.0.0" def test_suggest_version_fix_valid_package(self): """Test version fix suggestion with valid package name.""" result = ErrorFormatter._suggest_version_fix("requests>>invalid") assert result == "requests>=1.0.0" def test_suggest_version_fix_no_package_match(self): """Test version fix suggestion with no package match.""" result = ErrorFormatter._suggest_version_fix("@@invalid") assert result == "package>=1.0.0" class TestNetworkErrorFormatting: """Test network error formatting.""" def test_format_network_errors_empty_list(self): """Test formatting empty list of network errors.""" result = ErrorFormatter.format_network_errors([]) assert result == [] def test_format_network_errors_package_not_found(self): """Test formatting PackageNotFoundError.""" errors = [ { "type": "PackageNotFoundError", "package": "nonexistent-pkg", "message": "Package not found", "suggestions": ["requests", "httpx"], } ] result = ErrorFormatter.format_network_errors(errors) assert len(result) == 1 assert result[0].error_code == "package_not_found" assert "nonexistent-pkg" in result[0].message assert "requests" in result[0].suggestion assert result[0].severity == ErrorSeverity.ERROR assert result[0].recoverable is True def test_format_network_errors_package_not_found_no_suggestions(self): """Test formatting PackageNotFoundError without suggestions.""" errors = [ { "type": "PackageNotFoundError", "package": "nonexistent-pkg", "message": "Package not found", "suggestions": [], } ] result = ErrorFormatter.format_network_errors(errors) assert len(result) == 1 assert "Did you mean:" not in result[0].suggestion assert "Check the package name" in result[0].suggestion def test_format_network_errors_network_error(self): """Test formatting NetworkError.""" errors = [ { "type": "NetworkError", "package": "requests", "message": "Connection timeout", "retry_suggested": True, } ] result = ErrorFormatter.format_network_errors(errors) assert len(result) == 1 assert result[0].error_code == "network_error" assert "requests" in result[0].message assert "Connection timeout" in result[0].message assert "retried automatically" in result[0].suggestion assert result[0].severity == ErrorSeverity.ERROR def test_format_network_errors_network_error_no_retry(self): """Test formatting NetworkError without retry suggestion.""" errors = [ { "type": "NetworkError", "package": "requests", "message": "Connection failed", "retry_suggested": False, } ] result = ErrorFormatter.format_network_errors(errors) assert len(result) == 1 assert "retried automatically" not in result[0].suggestion assert "internet connection" in result[0].suggestion def test_format_network_errors_generic_error(self): """Test formatting generic error type.""" errors = [ { "type": "UnknownError", "package": "test-pkg", "message": "Something went wrong", } ] result = ErrorFormatter.format_network_errors(errors) assert len(result) == 1 assert result[0].error_code == "generic_error" assert "test-pkg" in result[0].message assert "Something went wrong" in result[0].message assert result[0].context["type"] == "UnknownError" class TestExceptionFormatting: """Test exception formatting.""" def test_format_exception_project_parsing_error(self): """Test formatting ProjectParsingError.""" exc = ProjectParsingError("Invalid TOML", Path("/test/pyproject.toml")) result = ErrorFormatter.format_exception(exc) assert result.error_code == "project_parse_error" assert "Invalid TOML" in result.message assert result.severity == ErrorSeverity.CRITICAL assert result.recoverable is False assert result.context["file_path"] == "/test/pyproject.toml" def test_format_exception_project_parsing_error_no_path(self): """Test formatting ProjectParsingError without file path.""" exc = ProjectParsingError("Invalid TOML", None) result = ErrorFormatter.format_exception(exc) assert result.context["file_path"] is None def test_format_exception_package_not_found_error(self): """Test formatting PackageNotFoundError.""" exc = PackageNotFoundError("Package not found") context = {"package": "requests"} result = ErrorFormatter.format_exception(exc, context) assert result.error_code == "package_not_found" assert "requests" in result.message assert result.severity == ErrorSeverity.ERROR assert result.recoverable is True assert result.context == context def test_format_exception_package_not_found_error_no_context(self): """Test formatting PackageNotFoundError without context.""" exc = PackageNotFoundError("Package not found") result = ErrorFormatter.format_exception(exc) assert "unknown" in result.message def test_format_exception_network_error(self): """Test formatting NetworkError.""" exc = NetworkError("Connection failed") context = {"package": "httpx"} result = ErrorFormatter.format_exception(exc, context) assert result.error_code == "network_error" assert "Connection failed" in result.message assert result.severity == ErrorSeverity.ERROR assert result.recoverable is True assert "retried automatically" in result.suggestion def test_format_exception_generic_exception(self): """Test formatting generic exception.""" exc = ValueError("Something went wrong") context = {"operation": "test"} result = ErrorFormatter.format_exception(exc, context) assert result.error_code == "unexpected_error" assert "Something went wrong" in result.message assert result.severity == ErrorSeverity.CRITICAL assert result.recoverable is False assert result.context["exception_type"] == "ValueError" def test_format_exception_no_context(self): """Test formatting exception without context.""" exc = RuntimeError("Runtime issue") result = ErrorFormatter.format_exception(exc) assert result.error_code == "unexpected_error" assert "Runtime issue" in result.message assert result.context["exception_type"] == "RuntimeError" class TestResponseFormatter: """Test ResponseFormatter functionality.""" def test_format_scan_response_success(self, mocker): """Test formatting successful scan response.""" # Mock scan result mock_result = mocker.Mock() mock_result.dependencies = [mocker.Mock(), mocker.Mock()] # 2 dependencies mock_result.dependencies[0].model_dump.return_value = { "name": "requests", "version": ">=2.0.0", } mock_result.dependencies[1].model_dump.return_value = { "name": "httpx", "version": ">=0.24.0", } mock_result.failed_deps = [] mock_result.successful_deps = 2 mock_result.partial_success = False mock_result.project_name = "test-project" mock_result.project_path = Path("/test/project") mock_result.warnings = [] mock_result.scan_timestamp = datetime(2025, 1, 8, 12, 0, 0) result = ResponseFormatter.format_scan_response(mock_result) assert result["success"] is True assert result["partial_success"] is False assert result["project_name"] == "test-project" assert result["project_path"] == "/test/project" assert len(result["dependencies"]) == 2 assert result["dependency_count"] == 2 assert len(result["errors"]) == 0 assert result["scan_timestamp"] == "2025-01-08T12:00:00" def test_format_scan_response_with_errors(self, mocker): """Test formatting scan response with dependency errors.""" # Mock scan result with failures mock_result = mocker.Mock() mock_result.dependencies = [mocker.Mock()] # 1 successful dependency mock_result.dependencies[0].model_dump.return_value = { "name": "requests", "version": ">=2.0.0", } mock_result.failed_deps = [ { "dependency_string": "bad@pkg", "error": "Invalid package name format: 'bad@pkg'", "source": "dependencies", } ] mock_result.successful_deps = 1 mock_result.partial_success = True mock_result.project_name = "test-project" mock_result.project_path = Path("/test/project") mock_result.warnings = ["Some warning"] mock_result.scan_timestamp = datetime(2025, 1, 8, 12, 0, 0) result = ResponseFormatter.format_scan_response(mock_result) assert result["success"] is True # Has some dependencies assert result["partial_success"] is True assert len(result["dependencies"]) == 1 assert len(result["errors"]) == 1 assert result["errors"][0]["code"] == "invalid_package_name" assert result["errors"][0]["severity"] == "warning" assert len(result["suggestions"]) == 1 def test_format_scan_response_no_dependencies(self, mocker): """Test formatting scan response with no successful dependencies.""" # Mock scan result with no successful dependencies mock_result = mocker.Mock() mock_result.dependencies = [] mock_result.failed_deps = [ { "dependency_string": "", "error": "Empty dependency string", "source": "dependencies", } ] mock_result.successful_deps = 0 mock_result.partial_success = False mock_result.project_name = "test-project" mock_result.project_path = Path("/test/project") mock_result.warnings = [] mock_result.scan_timestamp = datetime(2025, 1, 8, 12, 0, 0) result = ResponseFormatter.format_scan_response(mock_result) assert result["success"] is False assert result["dependency_count"] == 0 assert len(result["errors"]) == 1 assert result["errors"][0]["code"] == "empty_dependency" def test_format_scan_response_error_structure(self, mocker): """Test the structure of formatted errors in scan response.""" # Mock scan result with multiple error types mock_result = mocker.Mock() mock_result.dependencies = [] mock_result.failed_deps = [ { "dependency_string": "bad@pkg", "error": "Invalid package name format: 'bad@pkg'", "source": "dependencies", }, { "dependency_string": "pkg[unclosed", "error": "Unclosed bracket in extras: 'pkg[unclosed'", "source": "dev-dependencies", }, ] mock_result.successful_deps = 0 mock_result.partial_success = False mock_result.project_name = "test-project" mock_result.project_path = Path("/test/project") mock_result.warnings = [] mock_result.scan_timestamp = datetime(2025, 1, 8, 12, 0, 0) result = ResponseFormatter.format_scan_response(mock_result) # Check error structure assert len(result["errors"]) == 2 error1 = result["errors"][0] assert "message" in error1 assert "suggestion" in error1 assert "severity" in error1 assert "code" in error1 assert "recoverable" in error1 # Check suggestions are collected assert len(result["suggestions"]) == 2 assert all(suggestion for suggestion in result["suggestions"]) class TestEdgeCases: """Test edge cases and error conditions.""" def test_format_dependency_parsing_errors_missing_keys(self): """Test handling of malformed failure dictionaries.""" failed_deps = [ { # Missing some keys "dependency_string": "test-pkg", # "error" key missing "source": "dependencies", } ] result = ErrorFormatter.format_dependency_parsing_errors(failed_deps) assert len(result) == 1 # Should handle gracefully with defaults assert result[0].error_code == "dependency_parse_unknown" def test_format_network_errors_missing_keys(self): """Test handling of malformed network error dictionaries.""" errors = [ { "type": "NetworkError", # Missing "package" and "message" keys } ] result = ErrorFormatter.format_network_errors(errors) assert len(result) == 1 assert "unknown" in result[0].message assert result[0].error_code == "network_error" def test_pattern_matching_edge_cases(self): """Test regex pattern matching with edge cases.""" # Test with pattern that might not match result = ErrorFormatter._format_single_dependency_error( "test", "Some completely different error message", "dependencies" ) assert result.error_code == "dependency_parse_unknown" assert result.recoverable is True def test_suggestion_generation_edge_cases(self): """Test suggestion generation with edge cases.""" # Empty string result = ErrorFormatter._suggest_package_name_fix("") assert result == "package-name" # Only special characters result = ErrorFormatter._suggest_package_name_fix("@#$%") assert result == "package-name" # Very long string long_name = "a" * 200 + "@" + "b" * 200 result = ErrorFormatter._suggest_package_name_fix(long_name) assert "@" not in result assert "-" in result

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bradleyfay/autodoc-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server