diff --git a/.gitignore b/.gitignore index 6020c3c..f90a1bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ +.venv/ poetry.lock **/__pycache__/ diff --git a/src/womm/certify.py b/src/womm/certify.py index 800e019..42c6804 100644 --- a/src/womm/certify.py +++ b/src/womm/certify.py @@ -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( - f for f in path.rglob("*.rs") - if f.is_file() and _is_visible(f) and "tests" in f.parts - ) or ( - (path / "tests").is_dir() - and any((path / "tests").rglob("*.rs")) + 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 ( + (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,48 +468,85 @@ 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): - try: - r = subprocess.run( - ["make", "test"], - cwd=path, capture_output=True, text=True, timeout=120, - ) - if r.returncode == 0: - passed_runners.append("C/C++ (make test)") - elif "No rule to make target" in (r.stdout + r.stderr): - pass # no test target — skip gracefully - else: - snippet = (r.stdout + r.stderr).strip().split("\n")[-1] - failed_runners.append(f"C/C++: {snippet}") - except FileNotFoundError: - pass # make not installed — skip - except subprocess.TimeoutExpired: - failed_runners.append("C/C++: make test timed out") + 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", make_target], + cwd=path, capture_output=True, text=True, timeout=120, + ) + if r.returncode == 0: + passed_runners.append(f"C/C++ (make {make_target})") + _c_ran = True + break + elif "No rule to make target" in (r.stdout + r.stderr): + 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 # ctest not installed — skip + except subprocess.TimeoutExpired: + failed_runners.append("C/C++: ctest timed out") # --- Python (pytest, then unittest) --- - for cmd in ( - ["python", "-m", "pytest", "-x", "-q"], - ["python", "-m", "unittest", "discover", "-s", "."], - ): - try: - r = subprocess.run(cmd, cwd=path, capture_output=True, text=True, timeout=120) - if r.returncode == 0: - passed_runners.append("Python") + _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", "."], + ): + try: + r = subprocess.run(cmd, cwd=path, capture_output=True, text=True, timeout=120) + if r.returncode == 0: + passed_runners.append("Python") + break + elif r.returncode == 5 and "pytest" in cmd[2]: + continue # no tests collected — try unittest + else: + snippet = (r.stdout + r.stderr).strip().split("\n")[-1] + failed_runners.append(f"Python: {snippet}") + break + except FileNotFoundError: + continue + except subprocess.TimeoutExpired: + failed_runners.append("Python: timed out") break - elif r.returncode == 5 and "pytest" in cmd[2]: - continue # no tests collected — try unittest - else: - snippet = (r.stdout + r.stderr).strip().split("\n")[-1] - failed_runners.append(f"Python: {snippet}") - break - except FileNotFoundError: - continue - except subprocess.TimeoutExpired: - failed_runners.append("Python: timed out") - break if failed_runners: return CheckResult( @@ -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,