mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-29 16:16:08 +00:00
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>
156 lines
5.0 KiB
Python
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")
|