feat(certify): add multi-language support for syntax checks and tests
Expand certification checks to support multiple programming languages including Python, JavaScript/TypeScript, Go, Rust, Ruby, Java, and C/C++. This includes: - Language detection based on file extensions. - Syntax validation using language-specific tools (e.g., ast for Python, tsc for TypeScript, cargo check for Rust). - Test suite detection and execution for various frameworks (e.g., go test, cargo test, npm test, pytest/unittest). - Ignore common build/dependency directories during scans. - Update tests to reflect generalized syntax checking. - Add new badge assets for certification levels.
This commit is contained in:
parent
3791859422
commit
26b9299d44
@ -3,12 +3,49 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Directories to skip when walking the project tree.
|
||||||
|
_IGNORE_DIRS = {
|
||||||
|
".git", "node_modules", "__pycache__", ".venv", "venv",
|
||||||
|
"target", ".tox", ".eggs", "dist", "build", ".mypy_cache",
|
||||||
|
}
|
||||||
|
|
||||||
|
_EXT_TO_LANG: dict[str, str] = {
|
||||||
|
".py": "python",
|
||||||
|
".js": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
||||||
|
".jsx": "javascript",
|
||||||
|
".ts": "typescript", ".tsx": "typescript",
|
||||||
|
".go": "go",
|
||||||
|
".rs": "rust",
|
||||||
|
".java": "java",
|
||||||
|
".rb": "ruby",
|
||||||
|
".c": "c", ".h": "c",
|
||||||
|
".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", ".hpp": "cpp",
|
||||||
|
".cs": "csharp",
|
||||||
|
".kt": "kotlin", ".kts": "kotlin",
|
||||||
|
".sh": "shell", ".bash": "shell",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_languages(path: Path) -> set[str]:
|
||||||
|
"""Return the set of programming languages present in the project."""
|
||||||
|
langs: set[str] = set()
|
||||||
|
for f in path.rglob("*"):
|
||||||
|
if not f.is_file():
|
||||||
|
continue
|
||||||
|
if any(part in _IGNORE_DIRS for part in f.parts):
|
||||||
|
continue
|
||||||
|
lang = _EXT_TO_LANG.get(f.suffix.lower())
|
||||||
|
if lang:
|
||||||
|
langs.add(lang)
|
||||||
|
return langs
|
||||||
|
|
||||||
|
|
||||||
class Level(IntEnum):
|
class Level(IntEnum):
|
||||||
"""WOMM certification levels (Section 4.2)."""
|
"""WOMM certification levels (Section 4.2)."""
|
||||||
@ -81,12 +118,7 @@ def _check_project_exists(path: Path) -> CheckResult:
|
|||||||
|
|
||||||
|
|
||||||
def _check_has_source_files(path: Path) -> CheckResult:
|
def _check_has_source_files(path: Path) -> CheckResult:
|
||||||
extensions = {".py", ".js", ".ts", ".c", ".cpp", ".go", ".rs", ".java", ".rb", ".sh"}
|
found = bool(_detect_languages(path))
|
||||||
found = any(
|
|
||||||
f.suffix in extensions
|
|
||||||
for f in path.rglob("*")
|
|
||||||
if f.is_file() and ".git" not in f.parts
|
|
||||||
)
|
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
name="Contains source code",
|
name="Contains source code",
|
||||||
passed=found,
|
passed=found,
|
||||||
@ -99,45 +131,232 @@ def _check_has_source_files(path: Path) -> CheckResult:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _check_python_syntax(path: Path) -> CheckResult:
|
def _check_syntax(path: Path) -> CheckResult:
|
||||||
py_files = [
|
"""Check syntax/compilability for all detected languages."""
|
||||||
f for f in path.rglob("*.py")
|
langs = _detect_languages(path)
|
||||||
if f.is_file() and ".git" not in f.parts
|
|
||||||
]
|
if not langs:
|
||||||
if not py_files:
|
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
name="Python syntax check",
|
name="Syntax check",
|
||||||
passed=True,
|
passed=True,
|
||||||
message="No Python files to check. Blissful ignorance achieved.",
|
message="No source files to check. Blissful ignorance achieved.",
|
||||||
level=Level.BRONZE,
|
level=Level.BRONZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
for py_file in py_files:
|
checked: list[str] = []
|
||||||
|
skipped: list[str] = []
|
||||||
|
|
||||||
|
# --- Python ---
|
||||||
|
if "python" in langs:
|
||||||
|
py_files = [
|
||||||
|
f for f in path.rglob("*.py")
|
||||||
|
if f.is_file() and not any(p in _IGNORE_DIRS for p in f.parts)
|
||||||
|
]
|
||||||
|
checked.append("Python")
|
||||||
|
for py_file in py_files:
|
||||||
|
try:
|
||||||
|
ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||||
|
except SyntaxError as exc:
|
||||||
|
errors.append(f"{py_file.name}:{exc.lineno}: {exc.msg}")
|
||||||
|
|
||||||
|
# --- TypeScript (tsc --noEmit) ---
|
||||||
|
if "typescript" in langs and (path / "tsconfig.json").exists():
|
||||||
try:
|
try:
|
||||||
ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
r = subprocess.run(
|
||||||
except SyntaxError as exc:
|
["tsc", "--noEmit"],
|
||||||
errors.append(f"{py_file.name}:{exc.lineno}: {exc.msg}")
|
cwd=path, capture_output=True, text=True, timeout=60,
|
||||||
|
)
|
||||||
|
checked.append("TypeScript")
|
||||||
|
if r.returncode != 0:
|
||||||
|
snippet = (r.stdout + r.stderr).strip().split("\n")[0]
|
||||||
|
errors.append(f"TypeScript: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
skipped.append("TypeScript (tsc not found)")
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
errors.append("TypeScript: tsc timed out")
|
||||||
|
|
||||||
|
# --- JavaScript (node --check) ---
|
||||||
|
elif "javascript" in langs:
|
||||||
|
js_files = [
|
||||||
|
f for f in path.rglob("*.js")
|
||||||
|
if f.is_file() and not any(p in _IGNORE_DIRS for p in f.parts)
|
||||||
|
][:20]
|
||||||
|
if js_files:
|
||||||
|
try:
|
||||||
|
checked.append("JavaScript")
|
||||||
|
for js_file in js_files:
|
||||||
|
r = subprocess.run(
|
||||||
|
["node", "--check", str(js_file)],
|
||||||
|
capture_output=True, text=True, timeout=10,
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
snippet = r.stderr.strip().split("\n")[0]
|
||||||
|
errors.append(f"{js_file.name}: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
checked.remove("JavaScript")
|
||||||
|
skipped.append("JavaScript (node not found)")
|
||||||
|
|
||||||
|
# --- Go (go build) ---
|
||||||
|
if "go" in langs and (path / "go.mod").exists():
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["go", "build", "./..."],
|
||||||
|
cwd=path, capture_output=True, text=True, timeout=120,
|
||||||
|
)
|
||||||
|
checked.append("Go")
|
||||||
|
if r.returncode != 0:
|
||||||
|
snippet = r.stderr.strip().split("\n")[0]
|
||||||
|
errors.append(f"Go: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
skipped.append("Go (go not found)")
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
errors.append("Go: build timed out")
|
||||||
|
|
||||||
|
# --- Rust (cargo check) ---
|
||||||
|
if "rust" in langs and (path / "Cargo.toml").exists():
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["cargo", "check", "--quiet"],
|
||||||
|
cwd=path, capture_output=True, text=True, timeout=180,
|
||||||
|
)
|
||||||
|
checked.append("Rust")
|
||||||
|
if r.returncode != 0:
|
||||||
|
snippet = r.stderr.strip().split("\n")[0]
|
||||||
|
errors.append(f"Rust: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
skipped.append("Rust (cargo not found)")
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
errors.append("Rust: cargo check timed out")
|
||||||
|
|
||||||
|
# --- Ruby (ruby -c) ---
|
||||||
|
if "ruby" in langs:
|
||||||
|
rb_files = [
|
||||||
|
f for f in path.rglob("*.rb")
|
||||||
|
if f.is_file() and not any(p in _IGNORE_DIRS for p in f.parts)
|
||||||
|
][:20]
|
||||||
|
if rb_files:
|
||||||
|
try:
|
||||||
|
checked.append("Ruby")
|
||||||
|
for rb_file in rb_files:
|
||||||
|
r = subprocess.run(
|
||||||
|
["ruby", "-c", str(rb_file)],
|
||||||
|
capture_output=True, text=True, timeout=10,
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
snippet = r.stderr.strip().split("\n")[0]
|
||||||
|
errors.append(f"{rb_file.name}: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
if "Ruby" in checked:
|
||||||
|
checked.remove("Ruby")
|
||||||
|
skipped.append("Ruby (ruby not found)")
|
||||||
|
|
||||||
|
# --- C / C++ (make build) ---
|
||||||
|
_makefiles = [path / name for name in ("Makefile", "GNUmakefile", "makefile")]
|
||||||
|
if ("c" in langs or "cpp" in langs) and any(m.exists() for m in _makefiles):
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["make", "build"],
|
||||||
|
cwd=path, capture_output=True, text=True, timeout=120,
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
checked.append("C/C++")
|
||||||
|
elif "No rule to make target" in (r.stdout + r.stderr):
|
||||||
|
skipped.append("C/C++ (no 'build' target in Makefile)")
|
||||||
|
else:
|
||||||
|
checked.append("C/C++")
|
||||||
|
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
|
||||||
|
errors.append(f"C/C++: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
skipped.append("C/C++ (make not found)")
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
errors.append("C/C++: make build timed out")
|
||||||
|
|
||||||
|
lang_str = ", ".join(checked) if checked else ", ".join(sorted(langs))
|
||||||
|
skip_note = f" (skipped: {', '.join(skipped)})" if skipped else ""
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return CheckResult(
|
||||||
|
name="Syntax check",
|
||||||
|
passed=False,
|
||||||
|
message=f"Syntax errors in {lang_str}{skip_note} ({len(errors)} issue(s)): {'; '.join(errors[:3])}",
|
||||||
|
level=Level.BRONZE,
|
||||||
|
)
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
name="Python syntax check",
|
name="Syntax check",
|
||||||
passed=len(errors) == 0,
|
passed=True,
|
||||||
message=(
|
message=f"All {lang_str} source parses cleanly{skip_note}. The AST smiles upon you.",
|
||||||
"All Python files parse cleanly. The AST smiles upon you."
|
|
||||||
if not errors
|
|
||||||
else f"Syntax errors found ({len(errors)}): {'; '.join(errors[:3])}"
|
|
||||||
),
|
|
||||||
level=Level.BRONZE,
|
level=Level.BRONZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_visible(f: Path) -> bool:
|
||||||
|
return not any(part in _IGNORE_DIRS for part in f.parts)
|
||||||
|
|
||||||
|
|
||||||
def _check_tests_exist(path: Path) -> CheckResult:
|
def _check_tests_exist(path: Path) -> CheckResult:
|
||||||
test_indicators = [
|
def rglob_visible(pattern: str) -> list[Path]:
|
||||||
path / "tests",
|
return [f for f in path.rglob(pattern) if _is_visible(f)]
|
||||||
path / "test",
|
|
||||||
*path.rglob("test_*.py"),
|
# Python
|
||||||
*path.rglob("*_test.py"),
|
python_test = (
|
||||||
*path.rglob("tests.py"),
|
(path / "tests").is_dir()
|
||||||
]
|
or (path / "test").is_dir()
|
||||||
found = any(p.exists() for p in test_indicators)
|
or bool(rglob_visible("test_*.py"))
|
||||||
|
or bool(rglob_visible("*_test.py"))
|
||||||
|
or bool(rglob_visible("tests.py"))
|
||||||
|
)
|
||||||
|
# JavaScript / TypeScript
|
||||||
|
js_test = (
|
||||||
|
any(
|
||||||
|
f for f in path.rglob("*")
|
||||||
|
if f.is_file() and _is_visible(f)
|
||||||
|
and f.name.endswith((".test.js", ".spec.js", ".test.ts", ".spec.ts",
|
||||||
|
".test.jsx", ".spec.jsx", ".test.tsx", ".spec.tsx"))
|
||||||
|
)
|
||||||
|
or any(
|
||||||
|
d for d in path.rglob("__tests__")
|
||||||
|
if d.is_dir() and _is_visible(d)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Go
|
||||||
|
go_test = 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"))
|
||||||
|
)
|
||||||
|
# Java
|
||||||
|
java_test = (
|
||||||
|
(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 = (
|
||||||
|
(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 = (
|
||||||
|
any(m.exists() for m in _makefiles)
|
||||||
|
and (
|
||||||
|
bool(rglob_visible("test_*.c"))
|
||||||
|
or bool(rglob_visible("*_test.c"))
|
||||||
|
or bool(rglob_visible("test_*.cpp"))
|
||||||
|
or bool(rglob_visible("*_test.cpp"))
|
||||||
|
or (path / "tests").is_dir()
|
||||||
|
or (path / "test").is_dir()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
found = python_test or js_test or go_test or rust_test or java_test or ruby_test or c_test
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
name="Test suite exists",
|
name="Test suite exists",
|
||||||
passed=found,
|
passed=found,
|
||||||
@ -151,43 +370,156 @@ def _check_tests_exist(path: Path) -> CheckResult:
|
|||||||
|
|
||||||
|
|
||||||
def _check_tests_pass(path: Path) -> CheckResult:
|
def _check_tests_pass(path: Path) -> CheckResult:
|
||||||
# Try pytest, then unittest
|
"""Run all applicable test suites and report combined results."""
|
||||||
for cmd in (["python", "-m", "pytest", "-x", "-q"], ["python", "-m", "unittest", "discover", "-s", "."]):
|
passed_runners: list[str] = []
|
||||||
|
failed_runners: list[str] = []
|
||||||
|
|
||||||
|
# --- Go ---
|
||||||
|
if (path / "go.mod").exists():
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
r = subprocess.run(
|
||||||
cmd,
|
["go", "test", "./..."],
|
||||||
cwd=path,
|
cwd=path, capture_output=True, text=True, timeout=300,
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=120,
|
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if r.returncode == 0:
|
||||||
return CheckResult(
|
passed_runners.append("Go")
|
||||||
name="Tests pass",
|
|
||||||
passed=True,
|
|
||||||
message="All tests pass. On this machine, at least.",
|
|
||||||
level=Level.SILVER,
|
|
||||||
)
|
|
||||||
elif result.returncode == 5 and "pytest" in cmd[2]:
|
|
||||||
# pytest exit code 5 = no tests collected
|
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
snippet = (result.stdout + result.stderr).strip().split("\n")[-1]
|
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
|
||||||
return CheckResult(
|
failed_runners.append(f"Go: {snippet}")
|
||||||
name="Tests pass",
|
except FileNotFoundError:
|
||||||
passed=False,
|
pass # go not installed — skip
|
||||||
message=f"Tests failed. {snippet}",
|
except subprocess.TimeoutExpired:
|
||||||
level=Level.SILVER,
|
failed_runners.append("Go: timed out")
|
||||||
|
|
||||||
|
# --- Rust ---
|
||||||
|
if (path / "Cargo.toml").exists():
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["cargo", "test", "--quiet"],
|
||||||
|
cwd=path, capture_output=True, text=True, timeout=300,
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
passed_runners.append("Rust")
|
||||||
|
else:
|
||||||
|
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
|
||||||
|
failed_runners.append(f"Rust: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
failed_runners.append("Rust: timed out")
|
||||||
|
|
||||||
|
# --- Node.js (npm test) ---
|
||||||
|
pkg_json = path / "package.json"
|
||||||
|
if pkg_json.exists():
|
||||||
|
try:
|
||||||
|
scripts = json.loads(pkg_json.read_text(encoding="utf-8")).get("scripts", {})
|
||||||
|
if "test" in scripts:
|
||||||
|
r = subprocess.run(
|
||||||
|
["npm", "test"],
|
||||||
|
cwd=path, capture_output=True, text=True, timeout=120,
|
||||||
)
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
passed_runners.append("Node.js")
|
||||||
|
else:
|
||||||
|
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
|
||||||
|
failed_runners.append(f"Node.js: {snippet}")
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
pass
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
failed_runners.append("Node.js: timed out")
|
||||||
|
|
||||||
|
# --- Maven ---
|
||||||
|
if (path / "pom.xml").exists():
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["mvn", "test", "-q"],
|
||||||
|
cwd=path, capture_output=True, text=True, timeout=300,
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
passed_runners.append("Maven")
|
||||||
|
else:
|
||||||
|
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
|
||||||
|
failed_runners.append(f"Maven: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
failed_runners.append("Maven: timed out")
|
||||||
|
|
||||||
|
# --- Gradle ---
|
||||||
|
if (path / "build.gradle").exists() or (path / "build.gradle.kts").exists():
|
||||||
|
gradle_cmd = "./gradlew" if (path / "gradlew").exists() else "gradle"
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
[gradle_cmd, "test"],
|
||||||
|
cwd=path, capture_output=True, text=True, timeout=300,
|
||||||
|
)
|
||||||
|
if r.returncode == 0:
|
||||||
|
passed_runners.append("Gradle")
|
||||||
|
else:
|
||||||
|
snippet = (r.stdout + r.stderr).strip().split("\n")[-1]
|
||||||
|
failed_runners.append(f"Gradle: {snippet}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
failed_runners.append("Gradle: timed out")
|
||||||
|
|
||||||
|
# --- C / C++ (make test) ---
|
||||||
|
_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")
|
||||||
|
|
||||||
|
# --- 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")
|
||||||
|
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:
|
except FileNotFoundError:
|
||||||
continue
|
continue
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
return CheckResult(
|
failed_runners.append("Python: timed out")
|
||||||
name="Tests pass",
|
break
|
||||||
passed=False,
|
|
||||||
message="Tests timed out after 120s. The machine grew weary.",
|
if failed_runners:
|
||||||
level=Level.SILVER,
|
return CheckResult(
|
||||||
)
|
name="Tests pass",
|
||||||
|
passed=False,
|
||||||
|
message=f"Test failures detected: {'; '.join(failed_runners[:3])}",
|
||||||
|
level=Level.SILVER,
|
||||||
|
)
|
||||||
|
if passed_runners:
|
||||||
|
return CheckResult(
|
||||||
|
name="Tests pass",
|
||||||
|
passed=True,
|
||||||
|
message=f"All {', '.join(passed_runners)} tests pass. On this machine, at least.",
|
||||||
|
level=Level.SILVER,
|
||||||
|
)
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
name="Tests pass",
|
name="Tests pass",
|
||||||
passed=False,
|
passed=False,
|
||||||
@ -300,7 +632,7 @@ def _check_git_clean(path: Path) -> CheckResult:
|
|||||||
ALL_CHECKS = [
|
ALL_CHECKS = [
|
||||||
_check_project_exists,
|
_check_project_exists,
|
||||||
_check_has_source_files,
|
_check_has_source_files,
|
||||||
_check_python_syntax,
|
_check_syntax,
|
||||||
_check_tests_exist,
|
_check_tests_exist,
|
||||||
_check_tests_pass,
|
_check_tests_pass,
|
||||||
_check_readme_exists,
|
_check_readme_exists,
|
||||||
|
|||||||
@ -3,6 +3,9 @@ README.md
|
|||||||
WOMM-CERTIFICATE.txt
|
WOMM-CERTIFICATE.txt
|
||||||
WOMM-STANDARD-001.md
|
WOMM-STANDARD-001.md
|
||||||
pyproject.toml
|
pyproject.toml
|
||||||
|
assets/womm-seal-bronze.svg
|
||||||
|
assets/womm-seal-gold.svg
|
||||||
|
assets/womm-seal-platinum.svg
|
||||||
assets/womm-seal.svg
|
assets/womm-seal.svg
|
||||||
src/womm/__init__.py
|
src/womm/__init__.py
|
||||||
src/womm/badge.py
|
src/womm/badge.py
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from womm.certify import (
|
|||||||
certify,
|
certify,
|
||||||
_check_has_source_files,
|
_check_has_source_files,
|
||||||
_check_project_exists,
|
_check_project_exists,
|
||||||
_check_python_syntax,
|
_check_syntax,
|
||||||
_check_readme_exists,
|
_check_readme_exists,
|
||||||
_check_no_todo_comments,
|
_check_no_todo_comments,
|
||||||
)
|
)
|
||||||
@ -76,13 +76,13 @@ class TestIndividualChecks:
|
|||||||
result = _check_has_source_files(empty_project)
|
result = _check_has_source_files(empty_project)
|
||||||
assert not result.passed
|
assert not result.passed
|
||||||
|
|
||||||
def test_python_syntax_pass(self, bronze_project: Path) -> None:
|
def test_syntax_pass(self, bronze_project: Path) -> None:
|
||||||
result = _check_python_syntax(bronze_project)
|
result = _check_syntax(bronze_project)
|
||||||
assert result.passed
|
assert result.passed
|
||||||
|
|
||||||
def test_python_syntax_fail(self, tmp_path: Path) -> None:
|
def test_syntax_fail(self, tmp_path: Path) -> None:
|
||||||
(tmp_path / "bad.py").write_text("def oops(\n")
|
(tmp_path / "bad.py").write_text("def oops(\n")
|
||||||
result = _check_python_syntax(tmp_path)
|
result = _check_syntax(tmp_path)
|
||||||
assert not result.passed
|
assert not result.passed
|
||||||
|
|
||||||
def test_readme_exists_pass(self, tmp_path: Path) -> None:
|
def test_readme_exists_pass(self, tmp_path: Path) -> None:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user