Files
SuperClaude/superclaude/validators/test_runner.py
kazuki ca29e595fd feat: add comprehensive validation framework
Add validators package with 6 specialized validators:
- base.py: Abstract base validator with common patterns
- context_contract.py: PM mode context validation
- dep_sanity.py: Dependency consistency checks
- runtime_policy.py: Runtime policy enforcement
- security_roughcheck.py: Security vulnerability scanning
- test_runner.py: Automated test execution validation

Supports validation gates for quality assurance and risk mitigation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 03:52:40 +09:00

156 lines
5.0 KiB
Python

"""Test Runner Validator
Validates that:
- Unit tests exist for changes
- Tests pass before implementation is approved
- Test coverage is maintained
"""
from typing import Dict, Any, List, Optional
import subprocess
from pathlib import Path
from .base import Validator, ValidationResult
class TestRunnerValidator(Validator):
"""Validates test execution"""
def __init__(self):
super().__init__("Test Runner")
def validate(self, context: Dict[str, Any]) -> ValidationResult:
"""
Validate tests.
Context should contain:
- changes: File changes
- git_root: Repository root
- contract: Context Contract
- test_command: Optional custom test command
"""
changes = context.get("changes", {})
git_root = context.get("git_root")
test_command = context.get("test_command")
if not git_root:
return self._skip("No git root provided")
# Detect test files in changes
test_files = [
path for path in changes.keys()
if self._is_test_file(path)
]
# If no tests and no test files changed, skip
if not test_files and not test_command:
return self._warning("No tests detected for changes")
# Run tests
test_result = self._run_tests(git_root, test_command)
if test_result["success"]:
return self._pass(
"Tests passed",
details={
"test_files": test_files,
"output": test_result.get("output", "")[:500] # First 500 chars
}
)
else:
return self._fail(
"Tests failed",
details={
"test_files": test_files,
"output": test_result.get("output", "")[:1000], # First 1000 chars
"error": test_result.get("error", "")[:500]
},
suggestions=[
"Fix failing tests before proceeding",
"Review test output for specific failures"
]
)
def _is_test_file(self, file_path: str) -> bool:
"""Check if file is a test file"""
path = Path(file_path)
# Common test file patterns
test_patterns = [
"test_", # Python: test_*.py
"_test.", # Go: *_test.go
".test.", # JS/TS: *.test.js, *.test.ts
".spec.", # JS/TS: *.spec.js, *.spec.ts
"/tests/", # In tests directory
"/test/", # In test directory
"/__tests__/", # React convention
]
file_path_lower = file_path.lower()
return any(pattern in file_path_lower for pattern in test_patterns)
def _run_tests(self, git_root: Path, test_command: Optional[str] = None) -> Dict[str, Any]:
"""Run tests and return results"""
if test_command:
# Use custom test command
return self._execute_test_command(git_root, test_command)
# Auto-detect test framework
if (git_root / "package.json").exists():
return self._run_npm_tests(git_root)
elif (git_root / "pyproject.toml").exists():
return self._run_python_tests(git_root)
else:
return {
"success": False,
"output": "",
"error": "Could not detect test framework"
}
def _execute_test_command(self, git_root: Path, command: str) -> Dict[str, Any]:
"""Execute custom test command"""
try:
result = subprocess.run(
command,
shell=True,
cwd=git_root,
capture_output=True,
text=True,
timeout=300, # 5 minutes max
check=False
)
return {
"success": result.returncode == 0,
"output": result.stdout,
"error": result.stderr
}
except subprocess.TimeoutExpired:
return {
"success": False,
"output": "",
"error": "Test execution timed out (5 minutes)"
}
except Exception as e:
return {
"success": False,
"output": "",
"error": f"Test execution failed: {str(e)}"
}
def _run_npm_tests(self, git_root: Path) -> Dict[str, Any]:
"""Run npm/pnpm tests"""
# Try pnpm first, fall back to npm
if (git_root / "pnpm-lock.yaml").exists():
return self._execute_test_command(git_root, "pnpm test")
else:
return self._execute_test_command(git_root, "npm test")
def _run_python_tests(self, git_root: Path) -> Dict[str, Any]:
"""Run Python tests (pytest/unittest)"""
# Try UV first, fall back to pytest
if (git_root / "uv.lock").exists():
return self._execute_test_command(git_root, "uv run pytest")
else:
return self._execute_test_command(git_root, "pytest")