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:
parent
26b9299d44
commit
7c5edce532
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.idea/
|
.idea/
|
||||||
|
.venv/
|
||||||
poetry.lock
|
poetry.lock
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
|
|||||||
@ -295,11 +295,13 @@ def _is_visible(f: Path) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def _check_tests_exist(path: Path) -> CheckResult:
|
def _check_tests_exist(path: Path) -> CheckResult:
|
||||||
|
langs = _detect_languages(path)
|
||||||
|
|
||||||
def rglob_visible(pattern: str) -> list[Path]:
|
def rglob_visible(pattern: str) -> list[Path]:
|
||||||
return [f for f in path.rglob(pattern) if _is_visible(f)]
|
return [f for f in path.rglob(pattern) if _is_visible(f)]
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
python_test = (
|
python_test = "python" in langs and (
|
||||||
(path / "tests").is_dir()
|
(path / "tests").is_dir()
|
||||||
or (path / "test").is_dir()
|
or (path / "test").is_dir()
|
||||||
or bool(rglob_visible("test_*.py"))
|
or bool(rglob_visible("test_*.py"))
|
||||||
@ -307,7 +309,7 @@ def _check_tests_exist(path: Path) -> CheckResult:
|
|||||||
or bool(rglob_visible("tests.py"))
|
or bool(rglob_visible("tests.py"))
|
||||||
)
|
)
|
||||||
# JavaScript / TypeScript
|
# JavaScript / TypeScript
|
||||||
js_test = (
|
js_test = ("javascript" in langs or "typescript" in langs) and (
|
||||||
any(
|
any(
|
||||||
f for f in path.rglob("*")
|
f for f in path.rglob("*")
|
||||||
if f.is_file() and _is_visible(f)
|
if f.is_file() and _is_visible(f)
|
||||||
@ -320,31 +322,34 @@ def _check_tests_exist(path: Path) -> CheckResult:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Go
|
# 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 — tests/ subdir or inline test modules
|
||||||
rust_test = bool(
|
rust_test = "rust" in langs and (
|
||||||
|
any(
|
||||||
f for f in path.rglob("*.rs")
|
f for f in path.rglob("*.rs")
|
||||||
if f.is_file() and _is_visible(f) and "tests" in f.parts
|
if f.is_file() and _is_visible(f) and "tests" in f.parts
|
||||||
) or (
|
)
|
||||||
|
or (
|
||||||
(path / "tests").is_dir()
|
(path / "tests").is_dir()
|
||||||
and any((path / "tests").rglob("*.rs"))
|
and any((path / "tests").rglob("*.rs"))
|
||||||
)
|
)
|
||||||
|
)
|
||||||
# Java
|
# Java
|
||||||
java_test = (
|
java_test = "java" in langs and (
|
||||||
(path / "src" / "test").exists()
|
(path / "src" / "test").exists()
|
||||||
or bool(rglob_visible("*Test.java"))
|
or bool(rglob_visible("*Test.java"))
|
||||||
or bool(rglob_visible("*Tests.java"))
|
or bool(rglob_visible("*Tests.java"))
|
||||||
or bool(rglob_visible("*Spec.java"))
|
or bool(rglob_visible("*Spec.java"))
|
||||||
)
|
)
|
||||||
# Ruby
|
# Ruby
|
||||||
ruby_test = (
|
ruby_test = "ruby" in langs and (
|
||||||
(path / "spec").is_dir()
|
(path / "spec").is_dir()
|
||||||
or bool(rglob_visible("*_spec.rb"))
|
or bool(rglob_visible("*_spec.rb"))
|
||||||
or bool(rglob_visible("*_test.rb"))
|
or bool(rglob_visible("*_test.rb"))
|
||||||
)
|
)
|
||||||
# C / C++ — Makefile with a 'test' target, or conventional test file patterns
|
# C / C++ — Makefile with a 'test' target, or conventional test file patterns
|
||||||
_makefiles = [path / name for name in ("Makefile", "GNUmakefile", "makefile")]
|
_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)
|
any(m.exists() for m in _makefiles)
|
||||||
and (
|
and (
|
||||||
bool(rglob_visible("test_*.c"))
|
bool(rglob_visible("test_*.c"))
|
||||||
@ -463,28 +468,65 @@ def _check_tests_pass(path: Path) -> CheckResult:
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
failed_runners.append("Gradle: timed out")
|
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")]
|
_makefiles = [path / name for name in ("Makefile", "GNUmakefile", "makefile")]
|
||||||
_langs = _detect_languages(path)
|
_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:
|
try:
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
["make", "test"],
|
["make", make_target],
|
||||||
cwd=path, capture_output=True, text=True, timeout=120,
|
cwd=path, capture_output=True, text=True, timeout=120,
|
||||||
)
|
)
|
||||||
if r.returncode == 0:
|
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):
|
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:
|
else:
|
||||||
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
|
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
|
||||||
failed_runners.append(f"C/C++: {snippet}")
|
failed_runners.append(f"C/C++: {snippet}")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass # make not installed — skip
|
pass # ctest not installed — skip
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
failed_runners.append("C/C++: make test timed out")
|
failed_runners.append("C/C++: ctest timed out")
|
||||||
|
|
||||||
# --- Python (pytest, then unittest) ---
|
# --- 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 (
|
for cmd in (
|
||||||
["python", "-m", "pytest", "-x", "-q"],
|
["python", "-m", "pytest", "-x", "-q"],
|
||||||
["python", "-m", "unittest", "discover", "-s", "."],
|
["python", "-m", "unittest", "discover", "-s", "."],
|
||||||
@ -523,7 +565,7 @@ def _check_tests_pass(path: Path) -> CheckResult:
|
|||||||
return CheckResult(
|
return CheckResult(
|
||||||
name="Tests pass",
|
name="Tests pass",
|
||||||
passed=False,
|
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,
|
level=Level.SILVER,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -583,7 +625,14 @@ def _check_ci_config_exists(path: Path) -> CheckResult:
|
|||||||
path / ".travis.yml",
|
path / ".travis.yml",
|
||||||
path / "azure-pipelines.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(
|
return CheckResult(
|
||||||
name="CI configuration exists",
|
name="CI configuration exists",
|
||||||
passed=found,
|
passed=found,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user