modify framework

This commit is contained in:
김경종
2026-06-02 09:51:30 +09:00
parent 88d8613847
commit a292238675
13 changed files with 602 additions and 169 deletions
+16 -38
View File
@@ -1,14 +1,10 @@
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path
CHECKS = ("lint", "build", "test")
def _repo_root(cwd: Path) -> Path:
try:
root = subprocess.check_output(
@@ -22,25 +18,11 @@ def _repo_root(cwd: Path) -> Path:
return Path(root)
def _load_scripts(root: Path) -> dict[str, str]:
package_json = root / "package.json"
if not package_json.exists():
return {}
try:
package = json.loads(package_json.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
_deny(f"Invalid package.json: {exc}")
raise SystemExit(0) from exc
scripts = package.get("scripts", {})
if not isinstance(scripts, dict):
return {}
return {str(name): str(command) for name, command in scripts.items()}
def _is_git_commit(command: str) -> bool:
return re.search(r"\bgit(?:\s+(?:-[A-Za-z]\s+\S+|--[A-Za-z0-9-]+(?:=\S+)?))*\s+commit\b", command) is not None
return re.search(
r"^\s*git(?:\s+(?:-[A-Za-z]\s+\S+|--[A-Za-z0-9-]+(?:=\S+)?))*\s+commit\b",
command,
) is not None
def _deny(reason: str) -> None:
@@ -64,26 +46,22 @@ def _tail(text: str, limit: int = 1200) -> str:
return text[-limit:]
def _run_checks(root: Path, scripts: dict[str, str]) -> str | None:
npm = shutil.which("npm") or shutil.which("npm.cmd")
if npm is None:
return "npm was not found, so pre-commit checks could not run."
def _build_pre_commit_commands(root: Path) -> list[list[str]]:
return [
[sys.executable, "-m", "unittest", "discover", "-s", "scripts", "-p", "test_*.py"],
[sys.executable, "scripts/validate_workspace.py"],
]
for check in CHECKS:
if check not in scripts:
continue
result = subprocess.run(
[npm, "run", check],
cwd=root,
capture_output=True,
text=True,
)
def _run_checks(root: Path) -> str | None:
for command in _build_pre_commit_commands(root):
result = subprocess.run(command, cwd=root, capture_output=True, text=True)
if result.returncode != 0:
details = _tail(result.stdout + "\n" + result.stderr)
label = " ".join(command)
if details:
return f"npm run {check} failed:\n{details}"
return f"npm run {check} failed with exit code {result.returncode}."
return f"{label} failed:\n{details}"
return f"{label} failed with exit code {result.returncode}."
return None
@@ -100,7 +78,7 @@ def main() -> int:
cwd = Path(payload.get("cwd") or Path.cwd())
root = _repo_root(cwd)
failure = _run_checks(root, _load_scripts(root))
failure = _run_checks(root)
if failure:
_deny(f"PRE-COMMIT CHECKS: {failure}")
+67 -51
View File
@@ -4,19 +4,9 @@ import sys
from pathlib import Path
SOURCE_SUFFIXES = {".ts", ".tsx", ".js", ".jsx"}
TEST_SUFFIXES = ("ts", "tsx", "js", "jsx")
CONFIG_SUFFIXES = {".json", ".css", ".scss", ".md", ".yml", ".yaml"}
NEXT_SPECIAL_FILES = {
"layout.ts",
"layout.tsx",
"page.ts",
"page.tsx",
"loading.tsx",
"error.tsx",
"not-found.tsx",
"globals.css",
}
SOURCE_SUFFIXES = {".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx", ".ixx"}
TEST_SUFFIXES = {".h", ".hpp", ".hh", ".hxx", ".c", ".cc", ".cpp", ".cxx", ".ixx"}
CONFIG_SUFFIXES = {".json", ".md", ".yml", ".yaml", ".txt", ".cmake"}
def _repo_root(cwd: Path) -> Path:
@@ -72,13 +62,66 @@ def _normalize(path_text: str) -> str:
def _is_test_path(path_text: str) -> bool:
normalized = _normalize(path_text)
name = normalized.rsplit("/", 1)[-1]
path = Path(path_text)
return (
"__tests__/" in normalized
"/tests/" in f"/{normalized}"
or "/test/" in f"/{normalized}"
or name.endswith("_test.cpp")
or name.startswith("test_")
or ".test." in name
or ".spec." in name
or "test" in name
or "spec" in name
)
) and path.suffix.lower() in TEST_SUFFIXES
def _token(text: str) -> str:
return "".join(ch for ch in text.lower() if ch.isalnum())
def _module_token(path: Path) -> str:
parts = [part.lower() for part in path.parts]
for marker in ("include", "src"):
if marker not in parts:
continue
idx = parts.index(marker)
if marker == "include" and idx + 2 < len(parts) and parts[idx + 1] == "fesa":
return _token(parts[idx + 2])
if marker == "src" and idx + 1 < len(parts):
return _token(parts[idx + 1])
return ""
def _related_tokens(path: Path) -> set[str]:
tokens = {_token(_base_name(path))}
module = _module_token(path)
if module:
tokens.add(module)
return {token for token in tokens if token}
def _candidate_test_paths(paths: list[str], cwd: Path, root: Path) -> list[Path]:
candidates: list[Path] = []
for path_text in paths:
resolved = _resolve_path(path_text, cwd)
if _is_test_path(str(resolved)):
candidates.append(resolved)
for test_root_name in ("tests", "test"):
test_root = root / test_root_name
if not test_root.is_dir():
continue
for suffix in TEST_SUFFIXES:
candidates.extend(test_root.rglob(f"*{suffix}"))
return candidates
def _has_related_test(path: Path, candidate_tests: list[Path]) -> bool:
tokens = _related_tokens(path)
for test_path in candidate_tests:
test_token = _token(test_path.stem)
if any(token and token in test_token for token in tokens):
return True
return False
def _is_exempt(path_text: str) -> bool:
@@ -86,17 +129,13 @@ def _is_exempt(path_text: str) -> bool:
path = Path(path_text)
name = path.name.lower()
if _is_test_path(path_text):
if name == "cmakelists.txt":
return True
if name in NEXT_SPECIAL_FILES:
if _is_test_path(path_text):
return True
if path.suffix.lower() in CONFIG_SUFFIXES:
return True
if ".env" in name or ".config." in name:
return True
if any(token in name for token in ("tailwind", "postcss", "next.config", "tsconfig")):
return True
if "/types/" in normalized or name in {"types.ts", "types.d.ts"}:
if "/cmake/" in normalized:
return True
return False
@@ -110,38 +149,15 @@ def _resolve_path(path_text: str, cwd: Path) -> Path:
def _base_name(path: Path) -> str:
for suffix in (".tsx", ".ts", ".jsx", ".js"):
if path.name.endswith(suffix):
for suffix in sorted(SOURCE_SUFFIXES, key=len, reverse=True):
if path.name.lower().endswith(suffix):
return path.name[: -len(suffix)]
return path.stem
def _has_existing_test(path: Path, root: Path) -> bool:
directory = path.parent
parent = directory.parent
base = _base_name(path)
for ext in TEST_SUFFIXES:
if (directory / f"{base}.test.{ext}").exists():
return True
if (directory / f"{base}.spec.{ext}").exists():
return True
for ext in TEST_SUFFIXES:
if (parent / "__tests__" / f"{base}.test.{ext}").exists():
return True
if (directory / "__tests__" / f"{base}.test.{ext}").exists():
return True
for ext in TEST_SUFFIXES:
if (root / "src" / "__tests__" / f"{base}.test.{ext}").exists():
return True
return False
def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]:
missing_tests: list[str] = []
candidate_tests = _candidate_test_paths(paths, cwd, root)
for path_text in paths:
if _is_exempt(path_text):
continue
@@ -149,7 +165,7 @@ def _guarded_paths(paths: list[str], cwd: Path, root: Path) -> list[str]:
path = _resolve_path(path_text, cwd)
if path.suffix.lower() not in SOURCE_SUFFIXES:
continue
if not _has_existing_test(path, root):
if not _has_related_test(path, candidate_tests):
missing_tests.append(_base_name(path))
return missing_tests