198 lines
5.1 KiB
Python
Executable File
198 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Multi-language test runner hook.
|
||
|
||
Automatically detects test framework and runs tests.
|
||
Enforces minimum 80% code coverage.
|
||
|
||
Supported frameworks:
|
||
- Python: pytest
|
||
- JavaScript/TypeScript: jest, vitest
|
||
- Go: go test
|
||
"""
|
||
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Tuple
|
||
|
||
|
||
def detect_test_framework() -> Tuple[str, str]:
|
||
"""Detect test framework from project files.
|
||
|
||
Returns:
|
||
(language, framework) tuple
|
||
"""
|
||
# Python
|
||
if Path("pytest.ini").exists() or Path("pyproject.toml").exists():
|
||
return "python", "pytest"
|
||
|
||
# JavaScript/TypeScript
|
||
if Path("jest.config.js").exists() or Path("jest.config.ts").exists():
|
||
return "javascript", "jest"
|
||
if Path("vitest.config.js").exists() or Path("vitest.config.ts").exists():
|
||
return "javascript", "vitest"
|
||
if Path("package.json").exists():
|
||
# Check package.json for test script
|
||
return "javascript", "npm"
|
||
|
||
# Go
|
||
if Path("go.mod").exists():
|
||
return "go", "go-test"
|
||
|
||
return "unknown", "unknown"
|
||
|
||
|
||
def run_pytest() -> bool:
|
||
"""Run pytest with coverage."""
|
||
try:
|
||
result = subprocess.run(
|
||
[
|
||
"python",
|
||
"-m",
|
||
"pytest",
|
||
"tests/",
|
||
"--cov=src",
|
||
"--cov-fail-under=80",
|
||
"--cov-report=term-missing:skip-covered",
|
||
"--tb=short",
|
||
"-q",
|
||
],
|
||
capture_output=True,
|
||
text=True,
|
||
)
|
||
|
||
print(result.stdout)
|
||
if result.stderr:
|
||
print(result.stderr, file=sys.stderr)
|
||
|
||
return result.returncode == 0
|
||
except FileNotFoundError:
|
||
print("❌ pytest not installed. Run: pip install pytest pytest-cov")
|
||
return False
|
||
|
||
|
||
def run_jest() -> bool:
|
||
"""Run jest with coverage."""
|
||
try:
|
||
result = subprocess.run(
|
||
["npx", "jest", "--coverage", "--coverageThreshold", '{"global":{"lines":80}}'],
|
||
capture_output=True,
|
||
text=True,
|
||
)
|
||
|
||
print(result.stdout)
|
||
if result.stderr:
|
||
print(result.stderr, file=sys.stderr)
|
||
|
||
return result.returncode == 0
|
||
except FileNotFoundError:
|
||
print("❌ jest not installed. Run: npm install --save-dev jest")
|
||
return False
|
||
|
||
|
||
def run_vitest() -> bool:
|
||
"""Run vitest with coverage."""
|
||
try:
|
||
result = subprocess.run(
|
||
["npx", "vitest", "run", "--coverage"], capture_output=True, text=True
|
||
)
|
||
|
||
print(result.stdout)
|
||
if result.stderr:
|
||
print(result.stderr, file=sys.stderr)
|
||
|
||
return result.returncode == 0
|
||
except FileNotFoundError:
|
||
print("❌ vitest not installed. Run: npm install --save-dev vitest")
|
||
return False
|
||
|
||
|
||
def run_npm_test() -> bool:
|
||
"""Run npm test."""
|
||
try:
|
||
result = subprocess.run(["npm", "test"], capture_output=True, text=True)
|
||
|
||
print(result.stdout)
|
||
if result.stderr:
|
||
print(result.stderr, file=sys.stderr)
|
||
|
||
return result.returncode == 0
|
||
except FileNotFoundError:
|
||
print("❌ npm not found")
|
||
return False
|
||
|
||
|
||
def run_go_test() -> bool:
|
||
"""Run go test with coverage."""
|
||
try:
|
||
# Run tests with coverage
|
||
result = subprocess.run(
|
||
["go", "test", "-cover", "./...", "-coverprofile=coverage.out"],
|
||
capture_output=True,
|
||
text=True,
|
||
)
|
||
|
||
print(result.stdout)
|
||
|
||
if result.returncode != 0:
|
||
if result.stderr:
|
||
print(result.stderr, file=sys.stderr)
|
||
return False
|
||
|
||
# Check coverage percentage
|
||
cov_result = subprocess.run(
|
||
["go", "tool", "cover", "-func=coverage.out"], capture_output=True, text=True
|
||
)
|
||
|
||
# Extract total coverage from last line
|
||
lines = cov_result.stdout.strip().split("\n")
|
||
if lines:
|
||
last_line = lines[-1]
|
||
if "total:" in last_line:
|
||
coverage = float(last_line.split()[-1].rstrip("%"))
|
||
print(f"\nTotal coverage: {coverage}%")
|
||
|
||
if coverage < 80:
|
||
print(f"❌ Coverage {coverage}% below 80% threshold")
|
||
return False
|
||
|
||
return True
|
||
except FileNotFoundError:
|
||
print("❌ go not installed")
|
||
return False
|
||
|
||
|
||
def main():
|
||
"""Run tests based on detected framework."""
|
||
language, framework = detect_test_framework()
|
||
|
||
if language == "unknown":
|
||
print("⚠️ Could not detect test framework. Skipping tests.")
|
||
print("ℹ️ Create pytest.ini, jest.config.js, or go.mod to enable auto-testing")
|
||
sys.exit(0) # Don't fail, just skip
|
||
|
||
print(f"🧪 Running tests with {framework}...")
|
||
|
||
# Run tests
|
||
runners = {
|
||
"pytest": run_pytest,
|
||
"jest": run_jest,
|
||
"vitest": run_vitest,
|
||
"npm": run_npm_test,
|
||
"go-test": run_go_test,
|
||
}
|
||
|
||
success = runners[framework]()
|
||
|
||
if success:
|
||
print("✅ Tests passed with ≥80% coverage")
|
||
sys.exit(0)
|
||
else:
|
||
print("❌ Tests failed or coverage below 80%")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|