mcilspy/tests/test_docstrings.py
Ryan Malloy 3c21b9d640 test: fix test compatibility with security and architecture changes
- Add bypass_path_validation fixture for tests using mock paths
- Update find_ilspycmd_path references to use mcilspy.utils
- Fix wrapper fixture patches in timeout tests
- Update assertions for new output formats (pagination, etc.)
- Mark all taskmaster domains as merged in status.json

All 165 tests passing.
2026-02-08 11:47:14 -07:00

303 lines
10 KiB
Python

"""Tests for docstring coverage.
Verifies that all public functions and classes have docstrings.
Uses AST to introspect the source code.
"""
import inspect
import pytest
import mcilspy.ilspy_wrapper as wrapper_module
import mcilspy.metadata_reader as reader_module
import mcilspy.models as models_module
import mcilspy.utils as utils_module
# Import the modules we want to check
import mcilspy.server as server_module
def get_public_functions_and_classes(module):
"""Get all public functions and classes from a module.
Returns a list of (name, obj, has_docstring) tuples.
"""
results = []
for name in dir(module):
if name.startswith("_"):
continue
obj = getattr(module, name)
# Check if it's a function or class defined in this module
if not (inspect.isfunction(obj) or inspect.isclass(obj)):
continue
# Skip imported items
if hasattr(obj, "__module__") and obj.__module__ != module.__name__:
continue
has_docstring = bool(inspect.getdoc(obj))
results.append((name, obj, has_docstring))
return results
def get_public_methods(cls):
"""Get all public methods from a class.
Returns a list of (name, method, has_docstring) tuples.
"""
results = []
for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
if name.startswith("_") and not name.startswith("__"):
continue
if name.startswith("__") and name != "__init__":
continue
has_docstring = bool(inspect.getdoc(method))
results.append((name, method, has_docstring))
return results
class TestServerModuleDocstrings:
"""Tests for server.py docstring coverage."""
def test_all_mcp_tools_have_docstrings(self):
"""Verify all @mcp.tool() decorated functions have docstrings."""
# Find all functions decorated with @mcp.tool()
# These are the public API and MUST have docstrings
tools = [
server_module.check_ilspy_installation,
server_module.install_ilspy,
server_module.decompile_assembly,
server_module.list_types,
server_module.generate_diagrammer,
server_module.get_assembly_info,
server_module.search_types,
server_module.search_strings,
server_module.search_methods,
server_module.search_fields,
server_module.search_properties,
server_module.list_events,
server_module.list_resources,
server_module.get_metadata_summary,
]
missing_docstrings = []
for tool in tools:
docstring = inspect.getdoc(tool)
if not docstring:
missing_docstrings.append(tool.__name__)
assert not missing_docstrings, f"Tools missing docstrings: {missing_docstrings}"
def test_tool_docstrings_have_args_section(self):
"""Verify tool docstrings document their arguments."""
# Tools with parameters should have Args: section
tools_with_params = [
server_module.decompile_assembly,
server_module.list_types,
server_module.generate_diagrammer,
server_module.get_assembly_info,
server_module.search_types,
server_module.search_strings,
server_module.search_methods,
server_module.search_fields,
server_module.search_properties,
server_module.list_events,
server_module.list_resources,
server_module.get_metadata_summary,
]
missing_args = []
for tool in tools_with_params:
docstring = inspect.getdoc(tool)
sig = inspect.signature(tool)
# Get non-ctx parameters
params = [
p
for p in sig.parameters.values()
if p.name != "ctx" and p.name != "self"
]
if params and docstring and "Args:" not in docstring:
missing_args.append(tool.__name__)
assert not missing_args, f"Tools missing Args section: {missing_args}"
def test_helper_functions_have_docstrings(self):
"""Verify helper functions have docstrings."""
helpers = [
server_module.get_wrapper,
server_module._format_error,
utils_module.find_ilspycmd_path, # Moved to utils
server_module._check_dotnet_tools,
server_module._detect_platform,
server_module._try_install_dotnet_sdk,
]
missing_docstrings = []
for helper in helpers:
docstring = inspect.getdoc(helper)
if not docstring:
missing_docstrings.append(helper.__name__)
assert not missing_docstrings, f"Helpers missing docstrings: {missing_docstrings}"
class TestWrapperModuleDocstrings:
"""Tests for ilspy_wrapper.py docstring coverage."""
def test_wrapper_class_has_docstring(self):
"""Verify ILSpyWrapper class has a docstring."""
docstring = inspect.getdoc(wrapper_module.ILSpyWrapper)
assert docstring, "ILSpyWrapper class should have a docstring"
def test_wrapper_public_methods_have_docstrings(self):
"""Verify ILSpyWrapper public methods have docstrings."""
methods_to_check = [
"decompile",
"list_types",
"generate_diagrammer",
"get_assembly_info",
]
missing_docstrings = []
for method_name in methods_to_check:
method = getattr(wrapper_module.ILSpyWrapper, method_name, None)
if method:
docstring = inspect.getdoc(method)
if not docstring:
missing_docstrings.append(method_name)
assert not missing_docstrings, (
f"ILSpyWrapper methods missing docstrings: {missing_docstrings}"
)
class TestMetadataReaderDocstrings:
"""Tests for metadata_reader.py docstring coverage."""
def test_reader_class_has_docstring(self):
"""Verify MetadataReader class has a docstring."""
docstring = inspect.getdoc(reader_module.MetadataReader)
assert docstring, "MetadataReader class should have a docstring"
def test_reader_public_methods_have_docstrings(self):
"""Verify MetadataReader public methods have docstrings."""
methods_to_check = [
"get_assembly_metadata",
"list_methods",
"list_fields",
"list_properties",
"list_events",
"list_resources",
]
missing_docstrings = []
for method_name in methods_to_check:
method = getattr(reader_module.MetadataReader, method_name, None)
if method:
docstring = inspect.getdoc(method)
if not docstring:
missing_docstrings.append(method_name)
assert not missing_docstrings, (
f"MetadataReader methods missing docstrings: {missing_docstrings}"
)
class TestModelsDocstrings:
"""Tests for models.py docstring coverage."""
def test_pydantic_models_have_docstrings(self):
"""Verify Pydantic model classes have docstrings."""
models_to_check = [
models_module.DecompileRequest,
models_module.DecompileResponse,
models_module.ListTypesRequest,
models_module.ListTypesResponse,
models_module.TypeInfo,
models_module.AssemblyInfo,
]
missing_docstrings = []
for model in models_to_check:
docstring = inspect.getdoc(model)
if not docstring:
missing_docstrings.append(model.__name__)
# Just check that most have docstrings - Pydantic models are self-documenting
# through their field names
assert len(missing_docstrings) <= 2, (
f"Too many models missing docstrings: {missing_docstrings}"
)
class TestModuleDocstrings:
"""Tests for module-level docstrings."""
def test_all_modules_have_docstrings(self):
"""Verify all mcilspy modules have module-level docstrings."""
modules = [
server_module,
wrapper_module,
reader_module,
models_module,
]
missing_docstrings = []
for module in modules:
if not module.__doc__:
missing_docstrings.append(module.__name__)
# Just warn, don't fail - module docstrings are nice but not critical
if missing_docstrings:
pytest.skip(f"Modules missing docstrings (non-critical): {missing_docstrings}")
class TestDocstringQuality:
"""Tests for docstring quality (not just presence)."""
def test_tool_docstrings_not_empty(self):
"""Verify tool docstrings have meaningful content."""
tools = [
server_module.decompile_assembly,
server_module.list_types,
server_module.search_methods,
]
short_docstrings = []
for tool in tools:
docstring = inspect.getdoc(tool)
if docstring and len(docstring) < 50:
short_docstrings.append(f"{tool.__name__}: {len(docstring)} chars")
assert not short_docstrings, (
f"Tools have too-short docstrings: {short_docstrings}"
)
def test_docstrings_describe_purpose(self):
"""Verify key tool docstrings describe what the tool does."""
key_words = {
server_module.decompile_assembly: ["decompile", "assembly", "C#"],
server_module.list_types: ["types", "list", "class"],
server_module.search_methods: ["search", "method"],
}
missing_keywords = []
for tool, keywords in key_words.items():
docstring = inspect.getdoc(tool).lower() if inspect.getdoc(tool) else ""
for keyword in keywords:
if keyword.lower() not in docstring:
missing_keywords.append(f"{tool.__name__} missing '{keyword}'")
assert not missing_keywords, (
f"Docstrings missing expected keywords: {missing_keywords}"
)