- 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.
303 lines
10 KiB
Python
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}"
|
|
)
|