Files
Auditor/theauditor/test_frameworks.py

236 lines
10 KiB
Python

"""Test framework detection for various languages."""
import json
import re
from pathlib import Path
from typing import Any
from theauditor.manifest_parser import ManifestParser
from theauditor.framework_registry import TEST_FRAMEWORK_REGISTRY
def detect_test_framework(root: str | Path) -> dict[str, Any]:
"""Detect the test framework used in a project using unified registry approach.
Args:
root: Root directory of the project.
Returns:
Dictionary with framework info:
{
"name": str, # pytest, jest, rspec, go, junit, etc.
"language": str, # python, javascript, etc.
"cmd": str, # Command to run tests
}
"""
root = Path(root)
parser = ManifestParser()
# Parse all relevant manifests once
manifests = {}
manifest_files = {
"pyproject.toml": root / "pyproject.toml",
"package.json": root / "package.json",
"requirements.txt": root / "requirements.txt",
"requirements-dev.txt": root / "requirements-dev.txt",
"requirements-test.txt": root / "requirements-test.txt",
"setup.cfg": root / "setup.cfg",
"setup.py": root / "setup.py",
"tox.ini": root / "tox.ini",
"Gemfile": root / "Gemfile",
"Gemfile.lock": root / "Gemfile.lock",
"go.mod": root / "go.mod",
"pom.xml": root / "pom.xml",
"build.gradle": root / "build.gradle",
"build.gradle.kts": root / "build.gradle.kts",
}
for name, path in manifest_files.items():
if path.exists():
try:
if name.endswith('.toml'):
manifests[name] = parser.parse_toml(path)
elif name.endswith('.json'):
manifests[name] = parser.parse_json(path)
elif name.endswith('.cfg') or name.endswith('.ini'):
manifests[name] = parser.parse_ini(path)
elif name.endswith('.txt'):
manifests[name] = parser.parse_requirements_txt(path)
elif name in ['Gemfile', 'Gemfile.lock']:
with open(path, 'r', encoding='utf-8') as f:
manifests[name] = f.read()
elif name.endswith(('.xml', '.gradle', '.kts', '.mod', '.py')):
with open(path, 'r', encoding='utf-8') as f:
manifests[name] = f.read()
except Exception:
# Skip files that can't be parsed
continue
# Check each test framework in priority order
for tf_name, tf_config in TEST_FRAMEWORK_REGISTRY.items():
# Check config files first (highest confidence)
if "config_files" in tf_config:
for config_file in tf_config["config_files"]:
if (root / config_file).exists():
# Special handling for different test runners
cmd = tf_config.get("command", "")
# For JUnit, determine build tool
if tf_name == "junit":
if (root / "pom.xml").exists():
cmd = tf_config.get("command_maven", "mvn test")
elif (root / "build.gradle").exists() or (root / "build.gradle.kts").exists():
cmd = tf_config.get("command_gradle", "gradle test")
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": cmd
}
# Check config sections in manifests
if "config_sections" in tf_config:
for manifest_name, section_paths in tf_config.get("config_sections", {}).items():
if manifest_name in manifests:
for section_path in section_paths:
section = parser.extract_nested_value(manifests[manifest_name], section_path)
if section is not None:
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": tf_config.get("command", "")
}
# Check dependencies in manifests
if "detection_sources" in tf_config:
for manifest_name, search_configs in tf_config["detection_sources"].items():
if manifest_name not in manifests:
continue
manifest_data = manifests[manifest_name]
if search_configs == "line_search":
# Text search for requirements.txt or Gemfile
if isinstance(manifest_data, list):
for line in manifest_data:
if tf_name in line:
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": tf_config.get("command", "")
}
elif isinstance(manifest_data, str) and tf_name in manifest_data:
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": tf_config.get("command", "")
}
elif search_configs == "content_search":
# Content search for text files
if isinstance(manifest_data, str):
# Check for test framework patterns
if tf_config.get("content_patterns"):
for pattern in tf_config["content_patterns"]:
if pattern in manifest_data:
# Determine command based on build tool
cmd = tf_config.get("command", "")
if tf_name == "junit":
if (root / "pom.xml").exists():
cmd = tf_config.get("command_maven", "mvn test")
elif (root / "build.gradle").exists() or (root / "build.gradle.kts").exists():
cmd = tf_config.get("command_gradle", "gradle test")
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": cmd
}
elif search_configs == "exists":
# Just check if file exists (for go.mod)
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": tf_config.get("command", "")
}
else:
# Structured search for dependencies
for key_path in search_configs:
deps = parser.extract_nested_value(manifest_data, key_path)
if deps:
# Check if test framework is in dependencies
version = parser.check_package_in_deps(deps, tf_name)
if version:
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": tf_config.get("command", "")
}
# Check for directory markers
if "directory_markers" in tf_config:
for dir_marker in tf_config["directory_markers"]:
if (root / dir_marker.rstrip('/')).is_dir():
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": tf_config.get("command", "")
}
# Check for file patterns
if "file_patterns" in tf_config:
for pattern in tf_config["file_patterns"]:
# Use glob to find matching files
import glob
if glob.glob(str(root / pattern)):
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": tf_config.get("command", "")
}
# Fallback: Check for import patterns in source files (for unittest)
# This is last resort for frameworks like unittest that don't have manifest entries
max_files_to_check = 20
files_checked = 0
for tf_name, tf_config in TEST_FRAMEWORK_REGISTRY.items():
if "import_patterns" not in tf_config:
continue
# Find files matching the language
ext_map = {
"python": ["*.py"],
"javascript": ["*.js", "*.jsx", "*.ts", "*.tsx"],
"java": ["*.java"],
"go": ["*.go"],
"ruby": ["*.rb"],
}
extensions = ext_map.get(tf_config["language"], [])
for ext in extensions:
if files_checked >= max_files_to_check:
break
import glob
for file_path in glob.glob(str(root / "**" / ext), recursive=True)[:max_files_to_check]:
if files_checked >= max_files_to_check:
break
files_checked += 1
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read(2000) # Read first 2000 chars
for import_pattern in tf_config["import_patterns"]:
if import_pattern in content:
return {
"name": tf_name,
"language": tf_config["language"],
"cmd": tf_config.get("command", "")
}
except Exception:
continue
# Unknown framework
return {"name": "unknown", "language": "unknown", "cmd": ""}
# Old detection functions removed - now using unified registry-based detection