mcilspy/tests/integration/test_real_assembly.py
Ryan Malloy 70c4a4a39a test: comprehensive test suite for mcilspy MCP server
Add complete test coverage for the mcilspy package:

- T7: Create TestAssembly.dll fixture with known types/members
- T1: Integration tests using real assembly (metadata reader + ILSpy wrapper)
- T2: MCP tool tests with mocked wrapper for each @mcp.tool()
- T3: Error path tests for regex, file not found, invalid assemblies
- T4: Concurrency tests with asyncio.gather() for parallel operations
- T5: Docstring coverage tests using AST introspection
- T6: Timeout behavior tests for 5-minute subprocess timeout

Test summary:
- 147 tests passing
- 14 skipped (ilspycmd-dependent integration tests)
- 73% code coverage
- All ruff linting checks pass
2026-02-08 11:40:57 -07:00

380 lines
13 KiB
Python

"""Integration tests using the custom TestAssembly.dll fixture.
These tests exercise the full stack including ilspycmd calls.
Tests are skipped if ilspycmd is not installed.
"""
import pytest
from mcilspy.ilspy_wrapper import ILSpyWrapper
from mcilspy.metadata_reader import MetadataReader
from mcilspy.models import (
DecompileRequest,
EntityType,
LanguageVersion,
ListTypesRequest,
)
class TestMetadataReaderWithTestAssembly:
"""Test MetadataReader against our custom test assembly."""
def test_get_assembly_metadata(self, test_assembly_path):
"""Test reading metadata from test assembly."""
with MetadataReader(test_assembly_path) as reader:
meta = reader.get_assembly_metadata()
assert meta.name == "TestAssemblyProject"
assert meta.type_count > 0
assert meta.method_count > 0
def test_list_methods_finds_known_methods(self, test_assembly_path):
"""Test that we can find methods we know exist."""
with MetadataReader(test_assembly_path) as reader:
methods = reader.list_methods()
method_names = [m.name for m in methods]
# Check for methods we defined in TestClass
assert "DoSomething" in method_names
assert "GetGreeting" in method_names
assert "Add" in method_names
def test_list_methods_with_type_filter(self, test_assembly_path):
"""Test filtering methods by type."""
with MetadataReader(test_assembly_path) as reader:
methods = reader.list_methods(type_filter="TestClass")
# All methods should be from types containing "TestClass"
for method in methods:
assert "TestClass" in method.declaring_type
def test_list_methods_with_namespace_filter(self, test_assembly_path):
"""Test filtering methods by namespace."""
with MetadataReader(test_assembly_path) as reader:
methods = reader.list_methods(namespace_filter="SubNamespace")
# Should only find methods from SubNamespace
for method in methods:
assert method.namespace is not None
assert "SubNamespace" in method.namespace
def test_list_methods_public_only(self, test_assembly_path):
"""Test filtering for public methods only."""
with MetadataReader(test_assembly_path) as reader:
public_methods = reader.list_methods(public_only=True)
all_methods = reader.list_methods(public_only=False)
# Should have fewer public methods than total
assert len(public_methods) <= len(all_methods)
# All returned methods should be public
for method in public_methods:
assert method.is_public
def test_list_fields_finds_known_fields(self, test_assembly_path):
"""Test that we can find fields we defined."""
with MetadataReader(test_assembly_path) as reader:
fields = reader.list_fields()
field_names = [f.name for f in fields]
# Check for constants and fields we defined
assert "API_KEY" in field_names
assert "BASE_URL" in field_names
assert "MAX_RETRIES" in field_names
def test_list_fields_constants_only(self, test_assembly_path):
"""Test filtering for constant fields only."""
with MetadataReader(test_assembly_path) as reader:
constants = reader.list_fields(constants_only=True)
# All returned fields should be literals
for field in constants:
assert field.is_literal
const_names = [f.name for f in constants]
assert "API_KEY" in const_names
assert "MAX_RETRIES" in const_names
def test_list_properties_finds_known_properties(self, test_assembly_path):
"""Test that we can find properties we defined."""
with MetadataReader(test_assembly_path) as reader:
properties = reader.list_properties()
prop_names = [p.name for p in properties]
# Check for properties we defined
assert "Name" in prop_names
assert "Age" in prop_names
assert "IsActive" in prop_names
assert "ServiceName" in prop_names
def test_list_events_finds_known_events(self, test_assembly_path):
"""Test that we can find events we defined."""
with MetadataReader(test_assembly_path) as reader:
events = reader.list_events()
event_names = [e.name for e in events]
# Check for events we defined
assert "OnChange" in event_names
assert "OnMessage" in event_names
def test_list_resources_empty_for_test_assembly(self, test_assembly_path):
"""Test that test assembly has no embedded resources."""
with MetadataReader(test_assembly_path) as reader:
resources = reader.list_resources()
# Our simple test assembly has no resources
assert isinstance(resources, list)
class TestILSpyWrapperWithTestAssembly:
"""Integration tests for ILSpyWrapper using real ilspycmd calls."""
@pytest.fixture
def wrapper(self, skip_without_ilspycmd):
"""Get wrapper instance, skipping if ilspycmd not available."""
return ILSpyWrapper()
@pytest.mark.asyncio
async def test_list_types_finds_classes(self, wrapper, test_assembly_path):
"""Test listing classes from test assembly."""
request = ListTypesRequest(
assembly_path=test_assembly_path,
entity_types=[EntityType.CLASS],
)
response = await wrapper.list_types(request)
assert response.success
assert response.total_count > 0
type_names = [t.name for t in response.types]
assert "TestClass" in type_names
assert "TestServiceImpl" in type_names
assert "OuterClass" in type_names
@pytest.mark.asyncio
async def test_list_types_finds_interfaces(self, wrapper, test_assembly_path):
"""Test listing interfaces from test assembly."""
request = ListTypesRequest(
assembly_path=test_assembly_path,
entity_types=[EntityType.INTERFACE],
)
response = await wrapper.list_types(request)
assert response.success
type_names = [t.name for t in response.types]
assert "ITestService" in type_names
assert "IConfigurable" in type_names
@pytest.mark.asyncio
async def test_list_types_finds_structs(self, wrapper, test_assembly_path):
"""Test listing structs from test assembly."""
request = ListTypesRequest(
assembly_path=test_assembly_path,
entity_types=[EntityType.STRUCT],
)
response = await wrapper.list_types(request)
assert response.success
type_names = [t.name for t in response.types]
assert "TestStruct" in type_names
@pytest.mark.asyncio
async def test_list_types_finds_enums(self, wrapper, test_assembly_path):
"""Test listing enums from test assembly."""
request = ListTypesRequest(
assembly_path=test_assembly_path,
entity_types=[EntityType.ENUM],
)
response = await wrapper.list_types(request)
assert response.success
type_names = [t.name for t in response.types]
assert "TestEnum" in type_names
@pytest.mark.asyncio
async def test_list_types_finds_delegates(self, wrapper, test_assembly_path):
"""Test listing delegates from test assembly."""
request = ListTypesRequest(
assembly_path=test_assembly_path,
entity_types=[EntityType.DELEGATE],
)
response = await wrapper.list_types(request)
assert response.success
type_names = [t.name for t in response.types]
assert "TestDelegate" in type_names
@pytest.mark.asyncio
async def test_decompile_specific_type(self, wrapper, test_assembly_path):
"""Test decompiling a specific type."""
request = DecompileRequest(
assembly_path=test_assembly_path,
type_name="TestNamespace.TestClass",
language_version=LanguageVersion.LATEST,
)
response = await wrapper.decompile(request)
assert response.success
assert response.source_code is not None
# Check that decompiled code contains expected elements
source = response.source_code
assert "class TestClass" in source
assert "DoSomething" in source
assert "GetGreeting" in source
@pytest.mark.asyncio
async def test_decompile_entire_assembly(self, wrapper, test_assembly_path):
"""Test decompiling the entire assembly."""
request = DecompileRequest(
assembly_path=test_assembly_path,
language_version=LanguageVersion.LATEST,
)
response = await wrapper.decompile(request)
assert response.success
assert response.source_code is not None
# Check that all types are present
source = response.source_code
assert "TestClass" in source
assert "ITestService" in source
assert "TestStruct" in source
assert "TestEnum" in source
@pytest.mark.asyncio
async def test_decompile_to_il(self, wrapper, test_assembly_path):
"""Test decompiling to IL code."""
request = DecompileRequest(
assembly_path=test_assembly_path,
type_name="TestNamespace.TestClass",
show_il_code=True,
)
response = await wrapper.decompile(request)
assert response.success
assert response.source_code is not None
# IL code should contain IL-specific keywords
source = response.source_code
# IL typically shows .method, .field, etc.
assert ".class" in source or "IL_" in source
@pytest.mark.asyncio
async def test_decompile_to_output_dir(self, wrapper, test_assembly_path, temp_output_dir):
"""Test decompiling to an output directory."""
request = DecompileRequest(
assembly_path=test_assembly_path,
output_dir=temp_output_dir,
)
response = await wrapper.decompile(request)
assert response.success
assert response.output_path is not None
@pytest.mark.asyncio
async def test_decompile_with_project_structure(
self, wrapper, test_assembly_path, temp_output_dir
):
"""Test decompiling with project structure."""
request = DecompileRequest(
assembly_path=test_assembly_path,
output_dir=temp_output_dir,
create_project=True,
)
response = await wrapper.decompile(request)
assert response.success
@pytest.mark.asyncio
async def test_decompile_nonexistent_type(self, wrapper, test_assembly_path):
"""Test decompiling a type that doesn't exist."""
request = DecompileRequest(
assembly_path=test_assembly_path,
type_name="NonExistent.FakeClass",
)
response = await wrapper.decompile(request)
# Should still succeed but with empty or no matching output
# The actual behavior depends on ilspycmd version
assert response is not None
class TestIntegrationEndToEnd:
"""End-to-end integration tests covering complete workflows."""
@pytest.mark.asyncio
async def test_discover_and_decompile_workflow(
self, skip_without_ilspycmd, test_assembly_path
):
"""Test the typical workflow: list types, then decompile specific one."""
wrapper = ILSpyWrapper()
# Step 1: List all types
list_request = ListTypesRequest(
assembly_path=test_assembly_path,
entity_types=[EntityType.CLASS],
)
list_response = await wrapper.list_types(list_request)
assert list_response.success
assert len(list_response.types) > 0
# Step 2: Find TestServiceImpl
service_type = None
for t in list_response.types:
if t.name == "TestServiceImpl":
service_type = t
break
assert service_type is not None
# Step 3: Decompile it
decompile_request = DecompileRequest(
assembly_path=test_assembly_path,
type_name=service_type.full_name,
)
decompile_response = await wrapper.decompile(decompile_request)
assert decompile_response.success
assert decompile_response.source_code is not None
assert "TestServiceImpl" in decompile_response.source_code
assert "ITestService" in decompile_response.source_code
@pytest.mark.asyncio
async def test_metadata_and_decompile_combined(
self, skip_without_ilspycmd, test_assembly_path
):
"""Test using metadata reader and ILSpy wrapper together."""
# Use metadata reader for quick discovery
with MetadataReader(test_assembly_path) as reader:
methods = reader.list_methods(type_filter="TestClass")
add_method = None
for m in methods:
if m.name == "Add":
add_method = m
break
assert add_method is not None
assert add_method.is_static
# Use ILSpy for decompilation
wrapper = ILSpyWrapper()
request = DecompileRequest(
assembly_path=test_assembly_path,
type_name="TestNamespace.TestClass",
)
response = await wrapper.decompile(request)
assert response.success
# Verify the static method is in the output
assert "static" in response.source_code
assert "Add" in response.source_code