mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
432 lines
18 KiB
Python
432 lines
18 KiB
Python
"""Test to ensure no stub/fake/incomplete code exists in production."""
|
|
|
|
import ast
|
|
import re
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
|
|
import pytest
|
|
|
|
|
|
class StubDetector(ast.NodeVisitor):
|
|
"""AST visitor to detect stub implementations."""
|
|
|
|
def __init__(self, filepath: str):
|
|
self.filepath = filepath
|
|
self.issues: List[Tuple[int, str]] = []
|
|
self.in_test_file = "test" in filepath or "mock" in filepath.lower()
|
|
self.in_except_handler = False # Track if we're inside an except block
|
|
|
|
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
"""Track that we're inside an except handler (fallback stubs are OK)."""
|
|
old_in_except = self.in_except_handler
|
|
self.in_except_handler = True
|
|
self.generic_visit(node)
|
|
self.in_except_handler = old_in_except
|
|
|
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
"""Check function definitions for stub patterns."""
|
|
# Skip test files for certain checks
|
|
if self.in_test_file and node.name.startswith("test_"):
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
# Skip functions defined in except handlers (these are legitimate fallbacks)
|
|
if self.in_except_handler:
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
# Skip dunder methods (like __init__) that may be empty placeholders
|
|
if node.name.startswith("__") and node.name.endswith("__"):
|
|
self.generic_visit(node)
|
|
return
|
|
|
|
# Check for empty functions with just pass (but allow if has docstring)
|
|
if len(node.body) == 1 and isinstance(node.body[0], ast.Pass):
|
|
self.issues.append(
|
|
(node.lineno, f"Function '{node.name}' contains only 'pass' statement")
|
|
)
|
|
elif len(node.body) == 2:
|
|
# Check for docstring + pass (also a stub)
|
|
if (
|
|
isinstance(node.body[0], ast.Expr)
|
|
and isinstance(node.body[0].value, ast.Constant)
|
|
and isinstance(node.body[0].value.value, str)
|
|
and isinstance(node.body[1], ast.Pass)
|
|
):
|
|
# Has docstring + pass - this is a documented stub, skip it
|
|
pass
|
|
|
|
# Check for functions that just raise NotImplementedError
|
|
# But allow if it's an abstract method (has docstring explaining it)
|
|
if len(node.body) == 1 and isinstance(node.body[0], ast.Raise):
|
|
if isinstance(node.body[0].exc, ast.Call):
|
|
if (
|
|
hasattr(node.body[0].exc.func, "id")
|
|
and node.body[0].exc.func.id == "NotImplementedError"
|
|
):
|
|
# This is a legitimate abstract method pattern, skip it
|
|
pass
|
|
elif len(node.body) == 2:
|
|
# Docstring + NotImplementedError - legitimate abstract method
|
|
if (
|
|
isinstance(node.body[0], ast.Expr)
|
|
and isinstance(node.body[0].value, ast.Constant)
|
|
and isinstance(node.body[0].value.value, str)
|
|
and isinstance(node.body[1], ast.Raise)
|
|
):
|
|
# Has docstring + raise, this is a documented abstract method
|
|
pass
|
|
|
|
# Check for functions with only ellipsis
|
|
if len(node.body) == 1 and isinstance(node.body[0], ast.Expr):
|
|
if isinstance(node.body[0].value, ast.Constant):
|
|
if node.body[0].value.value is Ellipsis:
|
|
self.issues.append(
|
|
(node.lineno, f"Function '{node.name}' contains only ellipsis")
|
|
)
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
"""Check async function definitions."""
|
|
# Treat async functions same as regular functions
|
|
self.visit_FunctionDef(node)
|
|
|
|
|
|
def find_stub_patterns(filepath: Path) -> List[Tuple[int, str, str]]:
|
|
"""Find stub patterns in a Python file."""
|
|
issues = []
|
|
|
|
# Skip test files for most checks
|
|
is_test_file = "test" in filepath.name or "mock" in filepath.name.lower()
|
|
|
|
try:
|
|
content = filepath.read_text(encoding="utf-8")
|
|
except Exception:
|
|
return issues
|
|
|
|
# Regex patterns to find stub indicators (with case sensitivity flag)
|
|
# Note: We're looking for TODO/FIXME comments, not tool names containing "todo"
|
|
# Format: (pattern, message, case_insensitive)
|
|
patterns = [
|
|
# Match TODO/FIXME comments (case insensitive for comments)
|
|
(
|
|
r"#\s*(TODO|FIXME|STUB|FAKE|UNFINISHED|HACK|XXX)\s*:",
|
|
"contains {0} comment",
|
|
True,
|
|
),
|
|
(
|
|
r'assert\s+False,?\s*["\']Not implemented',
|
|
'has "Not implemented" assertion',
|
|
True,
|
|
),
|
|
]
|
|
|
|
# Additional patterns for non-test files
|
|
if not is_test_file:
|
|
patterns.extend(
|
|
[
|
|
(r"pass\s*#\s*(stub|fake)", "has stub/fake comment after pass", True),
|
|
# Only match uppercase "TODO"/"STUB" strings (case sensitive to avoid "todo" tool name)
|
|
(r'return\s+["\']TODO["\']', "returns TODO string", False),
|
|
(r'return\s+["\']STUB["\']', "returns STUB string", False),
|
|
(
|
|
r"return\s+None\s*#\s*(STUB|FAKE)",
|
|
"returns None with stub comment",
|
|
True,
|
|
),
|
|
]
|
|
)
|
|
|
|
lines = content.split("\n")
|
|
for line_num, line in enumerate(lines, 1):
|
|
for pattern, message, case_insensitive in patterns:
|
|
flags = re.IGNORECASE if case_insensitive else 0
|
|
if match := re.search(pattern, line, flags):
|
|
keyword = match.group(1) if match.groups() else "stub pattern"
|
|
issues.append((line_num, message.format(keyword), filepath.name))
|
|
|
|
# Parse AST for deeper inspection
|
|
try:
|
|
tree = ast.parse(content)
|
|
detector = StubDetector(str(filepath))
|
|
detector.visit(tree)
|
|
for line_num, message in detector.issues:
|
|
issues.append((line_num, message, filepath.name))
|
|
except SyntaxError:
|
|
pass # Ignore files with syntax errors
|
|
|
|
return issues
|
|
|
|
|
|
def get_python_files(root_dir: Path, exclude_dirs: set = None) -> List[Path]:
|
|
"""Get all Python files in directory, excluding certain directories."""
|
|
if exclude_dirs is None:
|
|
exclude_dirs = {
|
|
"__pycache__",
|
|
".git",
|
|
".tox",
|
|
".pytest_cache",
|
|
"build",
|
|
"dist",
|
|
"*.egg-info",
|
|
".venv",
|
|
"venv",
|
|
"node_modules",
|
|
".mypy_cache",
|
|
}
|
|
|
|
python_files = []
|
|
for path in root_dir.rglob("*.py"):
|
|
# Skip excluded directories
|
|
if any(excluded in path.parts for excluded in exclude_dirs):
|
|
continue
|
|
python_files.append(path)
|
|
|
|
return python_files
|
|
|
|
|
|
class TestNoStubs:
|
|
"""Test suite to ensure no stub implementations exist."""
|
|
|
|
def test_no_stub_functions_in_source(self):
|
|
"""Ensure no stub functions exist in source code."""
|
|
# Get the package root
|
|
package_root = Path(__file__).parent.parent / "hanzo_mcp"
|
|
|
|
if not package_root.exists():
|
|
pytest.skip(f"Package root {package_root} does not exist")
|
|
|
|
all_issues = []
|
|
python_files = get_python_files(package_root)
|
|
|
|
for filepath in python_files:
|
|
issues = find_stub_patterns(filepath)
|
|
for line_num, message, _filename in issues:
|
|
all_issues.append(
|
|
f"{filepath.relative_to(package_root.parent)}:{line_num} - {message}"
|
|
)
|
|
|
|
if all_issues:
|
|
report = "\n".join(all_issues)
|
|
pytest.fail(
|
|
f"Found {len(all_issues)} stub/incomplete implementations:\n{report}"
|
|
)
|
|
|
|
def test_critical_functions_implemented(self):
|
|
"""Ensure critical functions are actually implemented."""
|
|
package_root = Path(__file__).parent.parent / "hanzo_mcp"
|
|
|
|
# Critical modules and functions that must be implemented
|
|
critical_checks = [
|
|
("tools/__init__.py", "register_all_tools"),
|
|
("server.py", "__init__"),
|
|
("server.py", "run"),
|
|
("cli.py", "main"),
|
|
]
|
|
|
|
for module_path, function_name in critical_checks:
|
|
filepath = package_root / module_path
|
|
if not filepath.exists():
|
|
pytest.fail(f"Critical module {module_path} does not exist")
|
|
|
|
content = filepath.read_text()
|
|
|
|
# Check function exists - use a more flexible pattern
|
|
# Match 'def function_name(' with optional type hints
|
|
function_pattern = rf"def {function_name}\s*\("
|
|
if not re.search(function_pattern, content):
|
|
pytest.fail(f"Function {function_name} not found in {module_path}")
|
|
|
|
# Find the function body and check it's not a stub
|
|
# Use AST for accurate parsing
|
|
try:
|
|
tree = ast.parse(content)
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
if node.name == function_name:
|
|
# Check if function body is just pass, ellipsis, or NotImplementedError
|
|
if len(node.body) == 1:
|
|
body = node.body[0]
|
|
if isinstance(body, ast.Pass):
|
|
pytest.fail(
|
|
f"Function {function_name} in {module_path} contains only 'pass'"
|
|
)
|
|
if isinstance(body, ast.Expr) and isinstance(
|
|
body.value, ast.Constant
|
|
):
|
|
if body.value.value is Ellipsis:
|
|
pytest.fail(
|
|
f"Function {function_name} in {module_path} contains only '...'"
|
|
)
|
|
if isinstance(body, ast.Raise) and isinstance(
|
|
body.exc, ast.Call
|
|
):
|
|
if (
|
|
hasattr(body.exc.func, "id")
|
|
and body.exc.func.id == "NotImplementedError"
|
|
):
|
|
pytest.fail(
|
|
f"Function {function_name} in {module_path} raises NotImplementedError"
|
|
)
|
|
break
|
|
except SyntaxError:
|
|
pass # If we can't parse, skip AST check
|
|
|
|
def test_no_pytest_skip_in_non_test_files(self):
|
|
"""Ensure pytest.skip is only used in test files."""
|
|
package_root = Path(__file__).parent.parent / "hanzo_mcp"
|
|
|
|
for filepath in get_python_files(package_root):
|
|
# Skip test directories
|
|
if "test" in str(filepath):
|
|
continue
|
|
|
|
content = filepath.read_text()
|
|
if "pytest.skip" in content or "@pytest.mark.skip" in content:
|
|
pytest.fail(f"Found pytest.skip in non-test file: {filepath}")
|
|
|
|
def test_no_mock_implementations_in_production(self):
|
|
"""Ensure no mock implementations exist in production code."""
|
|
package_root = Path(__file__).parent.parent / "hanzo_mcp"
|
|
|
|
for filepath in get_python_files(package_root):
|
|
# Skip test directories, legitimate mock modules, and fallback implementations
|
|
if "test" in str(filepath) or "mock" in filepath.name.lower():
|
|
continue
|
|
|
|
content = filepath.read_text()
|
|
|
|
# Check for mock-related imports in production code
|
|
# Note: class Mock/Fake are OK if they're for fallback functionality
|
|
# (like MockContext for when Context is not serialized over MCP)
|
|
mock_patterns = [
|
|
r"from unittest\.mock import",
|
|
r"import unittest\.mock",
|
|
# Only flag clearly test-oriented mock patterns
|
|
r"def fake_\w+\s*\(",
|
|
r"def mock_\w+\s*\(",
|
|
r'return\s+["\']fake\w*["\']',
|
|
r'return\s+["\']mock\w*["\']',
|
|
]
|
|
|
|
for pattern in mock_patterns:
|
|
if re.search(pattern, content, re.IGNORECASE):
|
|
pytest.fail(
|
|
f"Found mock/fake pattern '{pattern}' in production file: {filepath}"
|
|
)
|
|
|
|
def test_all_tool_classes_have_run_method(self):
|
|
"""Ensure all tool classes have a proper run or call method."""
|
|
package_root = Path(__file__).parent.parent / "hanzo_mcp" / "tools"
|
|
|
|
if not package_root.exists():
|
|
pytest.skip("Tools directory does not exist")
|
|
|
|
issues = []
|
|
for filepath in get_python_files(package_root):
|
|
if "test" in str(filepath) or "__pycache__" in str(filepath):
|
|
continue
|
|
|
|
content = filepath.read_text()
|
|
|
|
try:
|
|
tree = ast.parse(content)
|
|
except SyntaxError:
|
|
continue
|
|
|
|
# Find all top-level Tool classes (not nested) and check for run/call methods
|
|
for node in tree.body:
|
|
if isinstance(node, ast.ClassDef):
|
|
class_name = node.name
|
|
# Only check classes that end with Tool
|
|
if not class_name.endswith("Tool"):
|
|
continue
|
|
# Skip Params/Config classes that are just type definitions
|
|
if "Params" in class_name or "Config" in class_name:
|
|
continue
|
|
# Skip abstract base classes
|
|
if "Base" in class_name or "Abstract" in class_name:
|
|
continue
|
|
# Skip adapter classes
|
|
if "Adapter" in class_name:
|
|
continue
|
|
|
|
# Check if class has run() or call() method (or execute as alias)
|
|
has_run_or_call = False
|
|
for item in node.body:
|
|
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
if item.name in ("run", "call", "execute"):
|
|
has_run_or_call = True
|
|
break
|
|
# Also check for method aliases like "call = execute"
|
|
if isinstance(item, ast.Assign):
|
|
for target in item.targets:
|
|
if isinstance(target, ast.Name) and target.id in (
|
|
"run",
|
|
"call",
|
|
):
|
|
has_run_or_call = True
|
|
break
|
|
|
|
if not has_run_or_call:
|
|
# If it inherits from any base class, the base likely provides call()
|
|
# Only flag classes with no inheritance or with Object-only inheritance
|
|
inherits_from_something_meaningful = len(node.bases) > 0
|
|
if inherits_from_something_meaningful:
|
|
# Check all base class names
|
|
for base in node.bases:
|
|
base_name = ""
|
|
if isinstance(base, ast.Name):
|
|
base_name = base.id
|
|
elif isinstance(base, ast.Attribute):
|
|
base_name = base.attr
|
|
# If any base ends with Tool, Base, Mixin it's likely OK
|
|
if any(
|
|
base_name.endswith(suffix)
|
|
for suffix in ("Tool", "Base", "Mixin")
|
|
):
|
|
inherits_from_something_meaningful = True
|
|
break
|
|
else:
|
|
# No bases - this is a standalone Tool class that needs run/call
|
|
issues.append(
|
|
f"Tool class {class_name} in {filepath.name} missing run() or call() method"
|
|
)
|
|
|
|
if issues:
|
|
pytest.fail(
|
|
f"Found {len(issues)} tool classes without run()/call() method:\n"
|
|
+ "\n".join(issues[:10])
|
|
)
|
|
|
|
def test_no_debug_prints_in_production(self):
|
|
"""Ensure no debug print statements in production code."""
|
|
package_root = Path(__file__).parent.parent / "hanzo_mcp"
|
|
|
|
for filepath in get_python_files(package_root):
|
|
# Skip test files
|
|
if "test" in str(filepath):
|
|
continue
|
|
|
|
content = filepath.read_text()
|
|
|
|
# Check for debug patterns
|
|
debug_patterns = [
|
|
(r"print\s*\([^)]*#\s*DEBUG", "debug print statement"),
|
|
(r"print\s*\([^)]*#\s*TODO", "TODO print statement"),
|
|
(r"print\s*\([^)]*#\s*REMOVE", "REMOVE print statement"),
|
|
(r"console\.log", "console.log statement"),
|
|
(r"debugger;?", "debugger statement"),
|
|
]
|
|
|
|
for pattern, description in debug_patterns:
|
|
if re.search(pattern, content, re.IGNORECASE):
|
|
pytest.fail(f"Found {description} in production file: {filepath}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run tests directly
|
|
pytest.main([__file__, "-v"])
|