"""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 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, server_module._find_ilspycmd_path, 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}" )