mcilspy/tests/test_server_tools.py
Ryan Malloy f15905b350 feat: add output truncation guard and method-level decompilation
Large types (e.g., 15.5M chars of IL from CyBGElaborator) crashed MCP
clients. Two fixes:

1. decompile_assembly now truncates output exceeding max_output_chars
   (default 100K), saves the full output to a temp file, and returns
   a preview with recovery instructions. Set max_output_chars=0 to
   disable.

2. New decompile_method tool extracts individual methods from a type.
   IL mode uses ECMA-335 .method/end-of-method delimiters. C# mode
   uses signature matching with brace-depth counting. Handles
   overloads automatically.

New module: il_parser.py with extract_il_method() and
extract_csharp_method() functions, keeping parsing logic separate
from server.py.
2026-03-02 17:19:42 -07:00

882 lines
30 KiB
Python

"""Tests for MCP server tool functions.
These tests exercise the @mcp.tool() decorated functions in server.py.
We mock the ILSpyWrapper to test the tool logic independently of ilspycmd.
"""
import os
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mcilspy import server
from mcilspy.models import (
AssemblyInfo,
DecompileResponse,
ListTypesResponse,
TypeInfo,
)
from mcilspy.utils import find_ilspycmd_path
# Fixture to bypass path validation for tests using mock paths
@pytest.fixture
def bypass_path_validation():
"""Bypass _validate_assembly_path for tests using mock wrapper."""
def passthrough(path):
return path
with patch.object(server, "_validate_assembly_path", side_effect=passthrough):
yield
class TestCheckIlspyInstallation:
"""Tests for check_ilspy_installation tool."""
@pytest.mark.asyncio
async def test_both_installed(self):
"""Test when both dotnet and ilspycmd are installed."""
mock_status = {
"dotnet_available": True,
"dotnet_version": "8.0.100",
"ilspycmd_available": True,
"ilspycmd_version": "8.2.0",
"ilspycmd_path": "/home/user/.dotnet/tools/ilspycmd",
}
with patch.object(server, "_check_dotnet_tools", return_value=mock_status):
result = await server.check_ilspy_installation()
assert "dotnet CLI" in result
assert "8.0.100" in result
assert "ilspycmd" in result
assert "8.2.0" in result
assert "ready to use" in result
@pytest.mark.asyncio
async def test_dotnet_not_installed(self):
"""Test when dotnet is not installed."""
mock_status = {
"dotnet_available": False,
"dotnet_version": None,
"ilspycmd_available": False,
"ilspycmd_version": None,
"ilspycmd_path": None,
}
with patch.object(server, "_check_dotnet_tools", return_value=mock_status):
result = await server.check_ilspy_installation()
assert "Not found" in result
assert "dotnet.microsoft.com" in result
@pytest.mark.asyncio
async def test_ilspycmd_not_installed(self):
"""Test when dotnet is installed but ilspycmd is not."""
mock_status = {
"dotnet_available": True,
"dotnet_version": "8.0.100",
"ilspycmd_available": False,
"ilspycmd_version": None,
"ilspycmd_path": None,
}
with patch.object(server, "_check_dotnet_tools", return_value=mock_status):
result = await server.check_ilspy_installation()
assert "ilspycmd" in result
assert "Not installed" in result
assert "install_ilspy" in result.lower() or "dotnet tool install" in result
@pytest.mark.usefixtures("bypass_path_validation")
class TestDecompileAssembly:
"""Tests for decompile_assembly tool."""
@pytest.mark.asyncio
async def test_successful_decompile(self):
"""Test successful decompilation returns formatted output."""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
type_name="MyClass",
source_code="public class MyClass { }",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly("/path/to/test.dll")
assert "Decompilation result" in result
assert "TestAssembly" in result
assert "public class MyClass" in result
assert "```csharp" in result
@pytest.mark.asyncio
async def test_decompile_with_output_dir(self):
"""Test decompilation to output directory."""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
output_path="/tmp/output",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly(
"/path/to/test.dll", output_dir="/tmp/output"
)
assert "successful" in result.lower()
assert "/tmp/output" in result
@pytest.mark.asyncio
async def test_decompile_failure(self):
"""Test failed decompilation returns error message."""
mock_response = DecompileResponse(
success=False,
assembly_name="TestAssembly",
error_message="Assembly not found",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly("/path/to/nonexistent.dll")
assert "failed" in result.lower()
assert "Assembly not found" in result
@pytest.mark.asyncio
async def test_decompile_with_type_name(self):
"""Test decompiling a specific type."""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
type_name="MyNamespace.MyClass",
source_code="namespace MyNamespace { public class MyClass { } }",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly(
"/path/to/test.dll", type_name="MyNamespace.MyClass"
)
assert "MyNamespace.MyClass" in result
@pytest.mark.asyncio
async def test_decompile_exception_handling(self):
"""Test that exceptions are handled gracefully."""
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(side_effect=RuntimeError("Test error"))
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly("/path/to/test.dll")
assert "Error" in result
assert "Test error" in result
@pytest.mark.usefixtures("bypass_path_validation")
class TestListTypes:
"""Tests for list_types tool."""
@pytest.mark.asyncio
async def test_list_types_success(self):
"""Test successful type listing."""
mock_types = [
TypeInfo(name="ClassA", full_name="NS.ClassA", kind="Class", namespace="NS"),
TypeInfo(name="ClassB", full_name="NS.ClassB", kind="Class", namespace="NS"),
TypeInfo(name="IService", full_name="NS.IService", kind="Interface", namespace="NS"),
]
mock_response = ListTypesResponse(
success=True,
types=mock_types,
total_count=3,
)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.list_types("/path/to/test.dll")
assert "Types in" in result
# New pagination format: "Showing X of Y types"
assert "Showing 3 of 3 types" in result or "Found 3 types" in result
assert "ClassA" in result
assert "ClassB" in result
assert "IService" in result
@pytest.mark.asyncio
async def test_list_types_grouped_by_namespace(self):
"""Test that types are grouped by namespace."""
mock_types = [
TypeInfo(name="ClassA", full_name="NS1.ClassA", kind="Class", namespace="NS1"),
TypeInfo(name="ClassB", full_name="NS2.ClassB", kind="Class", namespace="NS2"),
]
mock_response = ListTypesResponse(
success=True,
types=mock_types,
total_count=2,
)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.list_types("/path/to/test.dll")
# Should have namespace headers
assert "## NS1" in result
assert "## NS2" in result
@pytest.mark.asyncio
async def test_list_types_with_entity_types(self):
"""Test listing specific entity types."""
mock_response = ListTypesResponse(
success=True,
types=[TypeInfo(name="IService", full_name="NS.IService", kind="Interface", namespace="NS")],
total_count=1,
)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.list_types(
"/path/to/test.dll", entity_types=["interface"]
)
assert "IService" in result
mock_wrapper.list_types.assert_called_once()
@pytest.mark.asyncio
async def test_list_types_no_types_found(self):
"""Test when no types are found."""
mock_response = ListTypesResponse(
success=True,
types=[],
total_count=0,
error_message="No types found in assembly",
)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.list_types("/path/to/test.dll")
assert "No types found" in result
@pytest.mark.usefixtures("bypass_path_validation")
class TestSearchTypes:
"""Tests for search_types tool."""
@pytest.mark.asyncio
async def test_search_types_finds_matches(self):
"""Test searching types by pattern."""
mock_types = [
TypeInfo(name="UserService", full_name="NS.UserService", kind="Class", namespace="NS"),
TypeInfo(name="OrderService", full_name="NS.OrderService", kind="Class", namespace="NS"),
TypeInfo(name="Helper", full_name="NS.Helper", kind="Class", namespace="NS"),
]
mock_response = ListTypesResponse(
success=True,
types=mock_types,
total_count=3,
)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.search_types("/path/to/test.dll", pattern="Service")
assert "Search Results" in result
assert "UserService" in result
assert "OrderService" in result
assert "Helper" not in result
@pytest.mark.asyncio
async def test_search_types_case_insensitive(self):
"""Test case-insensitive search (default)."""
mock_types = [
TypeInfo(name="SERVICE", full_name="NS.SERVICE", kind="Class", namespace="NS"),
]
mock_response = ListTypesResponse(success=True, types=mock_types, total_count=1)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.search_types(
"/path/to/test.dll", pattern="service", case_sensitive=False
)
assert "SERVICE" in result
@pytest.mark.asyncio
async def test_search_types_with_namespace_filter(self):
"""Test searching with namespace filter."""
mock_types = [
TypeInfo(name="ClassA", full_name="App.Services.ClassA", kind="Class", namespace="App.Services"),
TypeInfo(name="ClassB", full_name="App.Models.ClassB", kind="Class", namespace="App.Models"),
]
mock_response = ListTypesResponse(success=True, types=mock_types, total_count=2)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.search_types(
"/path/to/test.dll", pattern="Class", namespace_filter="Services"
)
assert "ClassA" in result
assert "ClassB" not in result
@pytest.mark.asyncio
async def test_search_types_no_matches(self):
"""Test when no types match the pattern."""
mock_types = [
TypeInfo(name="Helper", full_name="NS.Helper", kind="Class", namespace="NS"),
]
mock_response = ListTypesResponse(success=True, types=mock_types, total_count=1)
mock_wrapper = MagicMock()
mock_wrapper.list_types = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.search_types("/path/to/test.dll", pattern="Service")
assert "No types found" in result
class TestSearchMethods:
"""Tests for search_methods tool (using metadata reader)."""
@pytest.mark.asyncio
async def test_search_methods_uses_metadata_reader(self, test_assembly_path):
"""Test that search_methods uses MetadataReader directly."""
result = await server.search_methods(test_assembly_path, pattern="Do")
assert "DoSomething" in result
@pytest.mark.asyncio
async def test_search_methods_filters_by_pattern(self, test_assembly_path):
"""Test method name pattern filtering."""
result = await server.search_methods(test_assembly_path, pattern="Get")
assert "GetGreeting" in result
assert "DoSomething" not in result
@pytest.mark.asyncio
async def test_search_methods_public_only(self, test_assembly_path):
"""Test filtering for public methods."""
result = await server.search_methods(
test_assembly_path, pattern="Method", public_only=True
)
# Should find public methods
assert "Method" in result or "No methods found" in result
class TestSearchFields:
"""Tests for search_fields tool."""
@pytest.mark.asyncio
async def test_search_fields_finds_constants(self, test_assembly_path):
"""Test finding constant fields."""
result = await server.search_fields(test_assembly_path, pattern="API")
assert "API_KEY" in result
@pytest.mark.asyncio
async def test_search_fields_constants_only(self, test_assembly_path):
"""Test filtering for constants only."""
result = await server.search_fields(
test_assembly_path, pattern="", constants_only=True
)
assert "const" in result
class TestSearchProperties:
"""Tests for search_properties tool."""
@pytest.mark.asyncio
async def test_search_properties(self, test_assembly_path):
"""Test searching for properties."""
result = await server.search_properties(test_assembly_path, pattern="Name")
assert "Name" in result
class TestListEvents:
"""Tests for list_events tool."""
@pytest.mark.asyncio
async def test_list_events(self, test_assembly_path):
"""Test listing events from assembly."""
result = await server.list_events(test_assembly_path)
assert "OnChange" in result or "No events" in result
class TestListResources:
"""Tests for list_resources tool."""
@pytest.mark.asyncio
async def test_list_resources_empty(self, test_assembly_path):
"""Test listing resources (test assembly has none)."""
result = await server.list_resources(test_assembly_path)
assert "No embedded resources" in result or "Embedded Resources" in result
class TestGetMetadataSummary:
"""Tests for get_metadata_summary tool."""
@pytest.mark.asyncio
async def test_get_metadata_summary(self, test_assembly_path):
"""Test getting metadata summary."""
result = await server.get_metadata_summary(test_assembly_path)
assert "Assembly Metadata Summary" in result
assert "Name" in result
assert "Version" in result
assert "Statistics" in result
assert "Types" in result
assert "Methods" in result
@pytest.mark.usefixtures("bypass_path_validation")
class TestGetAssemblyInfo:
"""Tests for get_assembly_info tool."""
@pytest.mark.asyncio
async def test_get_assembly_info_success(self):
"""Test getting assembly info successfully."""
mock_info = AssemblyInfo(
name="TestAssembly",
full_name="TestAssembly, Version=1.0.0.0",
location="/path/to/test.dll",
version="1.0.0.0",
target_framework=".NETStandard,Version=v2.0",
is_signed=False,
has_debug_info=False,
)
mock_wrapper = MagicMock()
mock_wrapper.get_assembly_info = AsyncMock(return_value=mock_info)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.get_assembly_info("/path/to/test.dll")
assert "Assembly Information" in result
assert "TestAssembly" in result
assert "1.0.0.0" in result
@pytest.mark.asyncio
async def test_get_assembly_info_exception(self):
"""Test handling of exceptions in get_assembly_info."""
mock_wrapper = MagicMock()
mock_wrapper.get_assembly_info = AsyncMock(
side_effect=FileNotFoundError("File not found")
)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.get_assembly_info("/nonexistent/file.dll")
assert "Error" in result
@pytest.mark.usefixtures("bypass_path_validation")
class TestGenerateDiagrammer:
"""Tests for generate_diagrammer tool."""
@pytest.mark.asyncio
async def test_generate_diagrammer_success(self):
"""Test successful diagram generation."""
mock_response = {
"success": True,
"output_directory": "/tmp/diagrammer",
}
mock_wrapper = MagicMock()
mock_wrapper.generate_diagrammer = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.generate_diagrammer("/path/to/test.dll")
assert "successfully" in result.lower()
assert "/tmp/diagrammer" in result
@pytest.mark.asyncio
async def test_generate_diagrammer_failure(self):
"""Test failed diagram generation."""
mock_response = {
"success": False,
"error_message": "Failed to generate diagram",
}
mock_wrapper = MagicMock()
mock_wrapper.generate_diagrammer = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.generate_diagrammer("/path/to/test.dll")
assert "Failed" in result
class TestHelperFunctions:
"""Tests for helper functions in server.py."""
def test_format_error_with_context(self):
"""Test _format_error with context."""
error = ValueError("test error")
result = server._format_error(error, "testing")
assert "Error" in result
assert "testing" in result
assert "test error" in result
def test_format_error_without_context(self):
"""Test _format_error without context."""
error = RuntimeError("something went wrong")
result = server._format_error(error)
assert "Error" in result
assert "something went wrong" in result
def test_find_ilspycmd_path_not_installed(self):
"""Test find_ilspycmd_path when not installed."""
with (
patch("mcilspy.utils.shutil.which", return_value=None),
patch("mcilspy.utils.os.path.isfile", return_value=False),
):
result = find_ilspycmd_path()
assert result is None
def test_find_ilspycmd_path_in_path(self):
"""Test find_ilspycmd_path when in PATH."""
with patch("mcilspy.utils.shutil.which", return_value="/usr/local/bin/ilspycmd"):
result = find_ilspycmd_path()
assert result == "/usr/local/bin/ilspycmd"
def test_detect_platform_linux(self):
"""Test platform detection on Linux."""
with (
patch("platform.system", return_value="Linux"),
patch("builtins.open", MagicMock()),
patch("shutil.which", return_value="/usr/bin/pacman"),
):
result = server._detect_platform()
assert result["system"] == "linux"
assert result["package_manager"] is not None
def test_detect_platform_windows(self):
"""Test platform detection on Windows."""
with (
patch("platform.system", return_value="Windows"),
patch("shutil.which", return_value=None),
):
result = server._detect_platform()
assert result["system"] == "windows"
@pytest.mark.usefixtures("bypass_path_validation")
class TestDecompileOutputTruncation:
"""Tests for decompile_assembly output truncation guard."""
@pytest.mark.asyncio
async def test_small_output_not_truncated(self):
"""Output within the limit should be returned as-is."""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
source_code="public class Small { }",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly("/path/to/test.dll")
assert "Output truncated" not in result
assert "public class Small { }" in result
@pytest.mark.asyncio
async def test_large_output_truncated(self):
"""Output exceeding the limit should be truncated with file path."""
large_code = "x" * 200_000 # 200K chars, exceeds 100K default
mock_response = DecompileResponse(
success=True,
assembly_name="LargeAssembly",
source_code=large_code,
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly("/path/to/test.dll")
assert "Output truncated" in result
assert "200,000 chars" in result
assert "mcilspy_full_output_" in result
assert "decompile_method" in result
assert "max_output_chars=0" in result
@pytest.mark.asyncio
async def test_truncation_saves_full_file(self):
"""The full output should be saved to a readable temp file."""
large_code = "public class Big { " + ("int field; " * 20_000) + "}"
mock_response = DecompileResponse(
success=True,
assembly_name="BigAssembly",
source_code=large_code,
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly("/path/to/test.dll")
# Extract file path from result
assert "mcilspy_full_output_" in result
# Find the path in the result
for line in result.split("\n"):
if "mcilspy_full_output_" in line:
# Extract path from markdown backticks
path = line.split("`")[1] if "`" in line else ""
if path and os.path.exists(path):
with open(path) as f:
saved = f.read()
assert saved == large_code
os.unlink(path) # cleanup
break
@pytest.mark.asyncio
async def test_truncation_disabled_with_zero(self):
"""Setting max_output_chars=0 should disable truncation."""
large_code = "x" * 200_000
mock_response = DecompileResponse(
success=True,
assembly_name="LargeAssembly",
source_code=large_code,
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly(
"/path/to/test.dll", max_output_chars=0
)
assert "Output truncated" not in result
assert large_code in result
@pytest.mark.asyncio
async def test_il_output_uses_il_fence(self):
"""IL output should use ```il code fences."""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
source_code=".method public void Main() {}",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_assembly(
"/path/to/test.dll", show_il_code=True
)
assert "```il" in result
@pytest.mark.usefixtures("bypass_path_validation")
class TestDecompileMethod:
"""Tests for decompile_method tool."""
@pytest.mark.asyncio
async def test_extracts_il_method(self):
"""Should extract a method from IL output."""
il_source = """\
.method public hidebysig instance void DoWork () cil managed
{
.maxstack 8
IL_0000: ret
} // end of method MyClass::DoWork
"""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
source_code=il_source,
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_method(
"/path/to/test.dll",
type_name="NS.MyClass",
method_name="DoWork",
)
assert "Method: DoWork" in result
assert "IL_0000: ret" in result
assert "```il" in result
@pytest.mark.asyncio
async def test_extracts_csharp_method(self):
"""Should extract a method from C# output."""
cs_source = """\
public class MyClass
{
public void DoWork()
{
Console.WriteLine("hello");
}
}
"""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
source_code=cs_source,
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_method(
"/path/to/test.dll",
type_name="NS.MyClass",
method_name="DoWork",
show_il_code=False,
)
assert "Method: DoWork" in result
assert "Console.WriteLine" in result
assert "```csharp" in result
@pytest.mark.asyncio
async def test_method_not_found(self):
"""Should return helpful message when method not found."""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
source_code=".method void Other() {} // end of method X::Other",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_method(
"/path/to/test.dll",
type_name="NS.MyClass",
method_name="NonExistent",
)
assert "not found" in result
assert "search_methods" in result
@pytest.mark.asyncio
async def test_decompile_failure_propagated(self):
"""Should propagate decompilation failures."""
mock_response = DecompileResponse(
success=False,
assembly_name="TestAssembly",
error_message="Type not found",
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_method(
"/path/to/test.dll",
type_name="NS.Missing",
method_name="Foo",
)
assert "failed" in result.lower()
assert "Type not found" in result
@pytest.mark.asyncio
async def test_empty_type_name_rejected(self):
"""Should reject empty type_name."""
result = await server.decompile_method(
"/path/to/test.dll",
type_name="",
method_name="Foo",
)
assert "Error" in result
@pytest.mark.asyncio
async def test_empty_method_name_rejected(self):
"""Should reject empty method_name."""
result = await server.decompile_method(
"/path/to/test.dll",
type_name="NS.MyClass",
method_name="",
)
assert "Error" in result
@pytest.mark.asyncio
async def test_multiple_overloads_labeled(self):
"""Multiple overloads should be labeled with overload numbers."""
il_source = """\
.method public hidebysig instance void Work (int32 x) cil managed
{
.maxstack 8
IL_0000: ret
} // end of method MyClass::Work
.method public hidebysig instance void Work (string s) cil managed
{
.maxstack 8
IL_0000: ret
} // end of method MyClass::Work
"""
mock_response = DecompileResponse(
success=True,
assembly_name="TestAssembly",
source_code=il_source,
)
mock_wrapper = MagicMock()
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
result = await server.decompile_method(
"/path/to/test.dll",
type_name="NS.MyClass",
method_name="Work",
)
assert "2 overloads" in result
assert "Overload 1" in result
assert "Overload 2" in result