feat(certify): enhance test detection, running, and CI config checks

- Add language detection to condition test existence checks
- Improve C/C++ test running: try make test/check, fallback to ctest
- Run Python tests only if tests are detected to avoid false failures
- Update no-test-runner message for clarity
- Add support for Gitea workflows in CI config detection
- chore: ignore .venv in .gitignore
This commit is contained in:
Greg Gauthier 2026-04-03 11:34:02 +01:00
parent 26b9299d44
commit 7c5edce532
2 changed files with 101 additions and 51 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea/
.venv/
poetry.lock
**/__pycache__/

View File

@ -295,11 +295,13 @@ def _is_visible(f: Path) -> bool:
def _check_tests_exist(path: Path) -> CheckResult:
langs = _detect_languages(path)
def rglob_visible(pattern: str) -> list[Path]:
return [f for f in path.rglob(pattern) if _is_visible(f)]
# Python
python_test = (
python_test = "python" in langs and (
(path / "tests").is_dir()
or (path / "test").is_dir()
or bool(rglob_visible("test_*.py"))
@ -307,7 +309,7 @@ def _check_tests_exist(path: Path) -> CheckResult:
or bool(rglob_visible("tests.py"))
)
# JavaScript / TypeScript
js_test = (
js_test = ("javascript" in langs or "typescript" in langs) and (
any(
f for f in path.rglob("*")
if f.is_file() and _is_visible(f)
@ -320,31 +322,34 @@ def _check_tests_exist(path: Path) -> CheckResult:
)
)
# Go
go_test = bool(rglob_visible("*_test.go"))
go_test = "go" in langs and bool(rglob_visible("*_test.go"))
# Rust — tests/ subdir or inline test modules
rust_test = bool(
rust_test = "rust" in langs and (
any(
f for f in path.rglob("*.rs")
if f.is_file() and _is_visible(f) and "tests" in f.parts
) or (
)
or (
(path / "tests").is_dir()
and any((path / "tests").rglob("*.rs"))
)
)
# Java
java_test = (
java_test = "java" in langs and (
(path / "src" / "test").exists()
or bool(rglob_visible("*Test.java"))
or bool(rglob_visible("*Tests.java"))
or bool(rglob_visible("*Spec.java"))
)
# Ruby
ruby_test = (
ruby_test = "ruby" in langs and (
(path / "spec").is_dir()
or bool(rglob_visible("*_spec.rb"))
or bool(rglob_visible("*_test.rb"))
)
# C / C++ — Makefile with a 'test' target, or conventional test file patterns
_makefiles = [path / name for name in ("Makefile", "GNUmakefile", "makefile")]
c_test = (
c_test = ("c" in langs or "cpp" in langs) and (
any(m.exists() for m in _makefiles)
and (
bool(rglob_visible("test_*.c"))
@ -463,28 +468,65 @@ def _check_tests_pass(path: Path) -> CheckResult:
except subprocess.TimeoutExpired:
failed_runners.append("Gradle: timed out")
# --- C / C++ (make test) ---
# --- C / C++ (make test → make check → ctest) ---
_makefiles = [path / name for name in ("Makefile", "GNUmakefile", "makefile")]
_langs = _detect_languages(path)
if ("c" in _langs or "cpp" in _langs) and any(m.exists() for m in _makefiles):
if "c" in _langs or "cpp" in _langs:
_c_ran = False
# Try make targets first
if any(m.exists() for m in _makefiles):
for make_target in ("test", "check"):
try:
r = subprocess.run(
["make", "test"],
["make", make_target],
cwd=path, capture_output=True, text=True, timeout=120,
)
if r.returncode == 0:
passed_runners.append("C/C++ (make test)")
passed_runners.append(f"C/C++ (make {make_target})")
_c_ran = True
break
elif "No rule to make target" in (r.stdout + r.stderr):
pass # no test target — skip gracefully
continue # target not found — try next
else:
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
failed_runners.append(f"C/C++: {snippet}")
_c_ran = True
break
except FileNotFoundError:
break # make not installed — skip
except subprocess.TimeoutExpired:
failed_runners.append(f"C/C++: make {make_target} timed out")
_c_ran = True
break
# Fall back to ctest (CMake projects)
if not _c_ran and (path / "CMakeLists.txt").exists():
try:
r = subprocess.run(
["ctest", "--test-dir", "build", "--output-on-failure"],
cwd=path, capture_output=True, text=True, timeout=120,
)
output = r.stdout + r.stderr
if "No tests were found" in output:
pass # ctest found nothing — skip
elif r.returncode == 0:
passed_runners.append("C/C++ (ctest)")
else:
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
failed_runners.append(f"C/C++: {snippet}")
except FileNotFoundError:
pass # make not installed — skip
pass # ctest not installed — skip
except subprocess.TimeoutExpired:
failed_runners.append("C/C++: make test timed out")
failed_runners.append("C/C++: ctest timed out")
# --- Python (pytest, then unittest) ---
_py_has_tests = "python" in _langs and (
(path / "tests").is_dir()
or (path / "test").is_dir()
or any(f for f in path.rglob("test_*.py") if _is_visible(f))
or any(f for f in path.rglob("*_test.py") if _is_visible(f))
or any(f for f in path.rglob("tests.py") if _is_visible(f))
)
if _py_has_tests:
for cmd in (
["python", "-m", "pytest", "-x", "-q"],
["python", "-m", "unittest", "discover", "-s", "."],
@ -523,7 +565,7 @@ def _check_tests_pass(path: Path) -> CheckResult:
return CheckResult(
name="Tests pass",
passed=False,
message="Could not find a test runner. Neither pytest nor unittest obliged.",
message="No test runner found. If tests exist, they're keeping a low profile.",
level=Level.SILVER,
)
@ -583,7 +625,14 @@ def _check_ci_config_exists(path: Path) -> CheckResult:
path / ".travis.yml",
path / "azure-pipelines.yml",
]
found = any(p.exists() for p in ci_paths)
gitea_workflows = path / ".gitea" / "workflows"
found = (
any(p.exists() for p in ci_paths)
or (
gitea_workflows.is_dir()
and any(gitea_workflows.glob("*.y*ml"))
)
)
return CheckResult(
name="CI configuration exists",
passed=found,