grade.py: не падать на машине без компилятора — ПРОПУСК с причиной, авто-отключение санитайзеров, .exe под Windows, WSL-подсказка

This commit is contained in:
Kodlo-chan
2026-09-23 17:48:28 +07:00
parent 165dd5619d
commit 75a10f9999
4 changed files with 155 additions and 18 deletions
+18
View File
@@ -20,6 +20,24 @@
- `grade.py` — автопроверка задач (ASAN/UBSAN, таймауты, PASS/FAIL + полный лог); - `grade.py` — автопроверка задач (ASAN/UBSAN, таймауты, PASS/FAIL + полный лог);
- `questions_export.txt` — вопросы квиза текстом. - `questions_export.txt` — вопросы квиза текстом.
## Требования
- **Linux / WSL / macOS**: `g++` (C++17), `gcc`, `gdb`, `python3`, `bash`, `awk`.
Одной строкой в Ubuntu: `sudo apt update && sudo apt install -y build-essential gdb python3`.
- **Windows-native (MinGW-w64/clang)**: работают задачи 01–06 и 10–13 (нужен `g++` в PATH),
а также карточки `facts_drill.py`. Задачи **07 (epoll), 08 (bash/awk) и 09 (ASAN+gdb)**
под Windows-native не запускаются — они помечаются `ПРОПУСК`, а не `FAIL`.
Полный прогон — в WSL:
```powershell
wsl --install -d Ubuntu
```
```bash
sudo apt update && sudo apt install -y build-essential gdb python3
cd /mnt/d/Projects/edu/eduplan-cpp-eltex/diag && python3 grade.py
```
- Если компилятора нет совсем, `grade.py` больше не падает трейсбеком: он печатает
`ПРОПУСК` с причиной по каждой задаче и подсказку, что доставить.
## Как начать ## Как начать
```bash ```bash
+3 -1
View File
@@ -54,7 +54,9 @@ python3 facts_drill.py --domain algo # только алгоритмы
python3 facts_drill.py --answers "20|62|ttl|..." # проверить строку ответов разом python3 facts_drill.py --answers "20|62|ttl|..." # проверить строку ответов разом
``` ```
Порядок: сначала квиз, потом задачи с 01 по 09 (диагностика), затем 10–13 как добор. Зависание = FAIL (есть таймауты). Порядок: сначала квиз, потом задачи с 01 по 09 (диагностика), затем 10–13 как добор.
Требования: Linux/WSL (или MinGW-w64 для задач 01–06, 10–13). Задачи 07/08/09 под
Windows-native пропускаются с пометкой `ПРОПУСК` — это не провал, гоняй их в WSL. Зависание = FAIL (есть таймауты).
Правила честности: без встроенных `popcount`/`reverse`-хелперов, без копирования чужих Правила честности: без встроенных `popcount`/`reverse`-хелперов, без копирования чужих
решений, `answer.txt` в задаче 09 — обязательная часть (оценивается ход разбора). решений, `answer.txt` в задаче 09 — обязательная часть (оценивается ход разбора).
+131 -15
View File
@@ -22,9 +22,90 @@ HERE = os.path.dirname(os.path.abspath(__file__))
TASKS = os.path.join(HERE, "tasks") TASKS = os.path.join(HERE, "tasks")
SUB = os.path.join(HERE, "submissions") SUB = os.path.join(HERE, "submissions")
WINDOWS = os.name == "nt"
LINUX = sys.platform.startswith("linux")
CXX = os.environ.get("CXX", "g++") CXX = os.environ.get("CXX", "g++")
CC = os.environ.get("CC", "gcc")
SAN = "-fsanitize=address,undefined -fno-omit-frame-pointer" SAN = "-fsanitize=address,undefined -fno-omit-frame-pointer"
FAST = False FAST = False
CXX_PATH = None
CC_PATH = None
HAS_SAN = False
# что нужно задаче, чтобы её вообще можно было запускать на этой машине
# (проверяется до компиляции; чего нет — задача попадает в ПРОПУСК, а не в FAIL)
REQUIRE = {
"07_epoll": ("linux+cxx", "epoll и unix-сокеты — только под Linux"),
"08_bash": ("bash", "нужны bash и awk/sort по большому логу"),
"09_gdb": ("cc-san", "нужны gcc/clang с ASAN/UBSAN"),
}
def tool(*names):
for n in names:
p = shutil.which(n)
if p:
return p
return None
def exe(name):
return name + ".exe" if WINDOWS else name
def run_path(name):
"""Путь для запуска локального бинаря (на Windows — с .exe и в кавычках-агностично)."""
return os.path.join(".", exe(name)) if not WINDOWS else exe(name)
def probe_sanitizers():
"""Может ли компилятор собрать простейший файл с ASAN/UBSAN."""
src = os.path.join(HERE, ".san_probe.cpp")
out = os.path.join(HERE, exe(".san_probe"))
try:
with open(src, "w", encoding="utf-8") as f:
f.write("int main(){int*p=new int[4];p[0]=1;int v=p[0];delete[]p;return v-1;}\n")
r = subprocess.run([CXX, "-std=c++17", "-O0"] + SAN.split() + [src, "-o", out],
capture_output=True, text=True, timeout=120)
ok = r.returncode == 0
except Exception:
ok = False
finally:
for p in (src, out):
try:
os.remove(p)
except OSError:
pass
return ok
def requirements_met(name):
"""(можно ли запускать, причина пропуска)"""
need, why = REQUIRE.get(name, (None, None))
if need == "linux+cxx":
if not LINUX:
return False, why
if not CXX_PATH:
return False, "не найден компилятор C++ (g++/clang++)"
return True, None
if need == "bash":
if WINDOWS:
return False, why + " (под Windows — только в WSL)"
if not tool("bash"):
return False, "не найден bash"
if not tool("awk"):
return False, "не найден awk"
return True, None
if need == "cc-san":
if not CC_PATH:
return False, "не найден gcc/clang"
if not HAS_SAN:
return False, "компилятор не умеет ASAN/UBSAN (MinGW/MSVC — WSL нужен)"
return True, None
if not CXX_PATH:
return False, "не найден компилятор C++ (g++/clang++)"
return True, None
# имя задачи: (тип, что запускать, таймаут, флаги компиляции) # имя задачи: (тип, что запускать, таймаут, флаги компиляции)
SPEC = { SPEC = {
@@ -49,6 +130,9 @@ def sh(cmd, cwd, timeout):
try: try:
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout) r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
return r.returncode, r.stdout + r.stderr, time.time() - t0, False return r.returncode, r.stdout + r.stderr, time.time() - t0, False
except FileNotFoundError as e:
return 127, f"[НЕТ ПРОГРАММЫ] {e.filename or cmd[0]}: не найдена в PATH\n" \
f"Проверь установку компилятора/утилиты или запусти под WSL.", time.time() - t0, False
except subprocess.TimeoutExpired as e: except subprocess.TimeoutExpired as e:
out = (e.stdout or b"").decode(errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "") out = (e.stdout or b"").decode(errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "")
return None, out + "\n[TIMEOUT] превышено время %s c" % timeout, time.time() - t0, True return None, out + "\n[TIMEOUT] превышено время %s c" % timeout, time.time() - t0, True
@@ -68,13 +152,14 @@ def run_task(name, root):
extra = [] if FAST else ["-fsanitize=thread"] extra = [] if FAST else ["-fsanitize=thread"]
elif not FAST: elif not FAST:
extra = SAN.split() extra = SAN.split()
cmd = [CXX] + flags + (extra or []) + [test, "-o", "t_" + name] binname = "t_" + name
cmd = [CXX] + flags + (extra or []) + [test, "-o", binname]
rc, out, secs, timed_out = sh(cmd, d, timeout) rc, out, secs, timed_out = sh(cmd, d, timeout)
log.append("$ " + " ".join(cmd) + "\n" + out) log.append("$ " + " ".join(cmd) + "\n" + out)
if rc != 0: if rc != 0:
return False, log, secs, timed_out return False, log, secs, timed_out
rc, out, secs2, timed_out = sh(["./t_" + name], d, timeout) rc, out, secs2, timed_out = sh([run_path(binname)], d, timeout)
log.append("$ ./t_" + name + "\n" + out) log.append("$ " + run_path(binname) + "\n" + out)
secs += secs2 secs += secs2
if "unsupported VMA" in out and not FAST: if "unsupported VMA" in out and not FAST:
# ThreadSanitizer не поддерживает этот размер адресного пространства # ThreadSanitizer не поддерживает этот размер адресного пространства
@@ -84,8 +169,8 @@ def run_task(name, root):
rc, out2, s, to = sh(cmd2, d, timeout) rc, out2, s, to = sh(cmd2, d, timeout)
log.append("$ " + " ".join(cmd2) + "\n# TSan недоступен на этой платформе, откат на ASAN\n" + out2) log.append("$ " + " ".join(cmd2) + "\n# TSan недоступен на этой платформе, откат на ASAN\n" + out2)
if rc == 0: if rc == 0:
rc, out2, s2, to = sh(["./t_" + name], d, timeout) rc, out2, s2, to = sh([run_path("t_" + name)], d, timeout)
log.append("$ ./t_" + name + "\n" + out2) log.append("$ " + run_path("t_" + name) + "\n" + out2)
secs += s2 secs += s2
elif kind == "bin": elif kind == "bin":
# epoll-сервер: solution.cpp -> бинарь solution, затем python-тест # epoll-сервер: solution.cpp -> бинарь solution, затем python-тест
@@ -110,7 +195,7 @@ def run_task(name, root):
def main(): def main():
global FAST global FAST, CXX, CC, CXX_PATH, CC_PATH, HAS_SAN
args = [a for a in sys.argv[1:]] args = [a for a in sys.argv[1:]]
FAST = "--fast" in args FAST = "--fast" in args
do_submit = "--submit" in args do_submit = "--submit" in args
@@ -120,25 +205,56 @@ def main():
else: else:
names = list(SPEC) names = list(SPEC)
CXX_PATH = tool(CXX, "g++", "clang++", "c++", "x86_64-w64-mingw32-g++")
CC_PATH = tool(CC, "gcc", "clang", "cc", "x86_64-w64-mingw32-gcc")
if CXX_PATH:
CXX = CXX_PATH
if CC_PATH:
CC = CC_PATH
os.environ["CC"] = CC
HAS_SAN = probe_sanitizers() if CXX_PATH else False
if not FAST and CXX_PATH and not HAS_SAN:
print("# Компилятор не собирает с ASAN/UBSAN — санитайзеры отключены автоматически.")
FAST = True
if WINDOWS:
print("# Windows-native сборка: работают задачи с компилятором C++ (01-06, 10-13).")
print("# 07 (epoll), 08 (bash/awk) и 09 (ASAN+gdb) требуют Linux — запускай их в WSL.")
if not CXX_PATH:
print("# Не найден компилятор C++ (g++/clang++). Поставь build-essential/MinGW-w64")
print("# или запусти задачи в WSL: wsl --install -d Ubuntu")
results = {} results = {}
print(f"{'задача':12s} {'итог':7s} {'проверок':9s} {'FAIL':5s} {'время':8s}") skipped = []
print(f"{'задача':12s} {'итог':8s} {'проверок':9s} {'FAIL':5s} {'время':8s}")
for n in names: for n in names:
can, why = requirements_met(n)
if not can:
results[n] = {"pass": False, "skipped": True, "reason": why,
"checks_ok": 0, "checks_fail": 0, "seconds": 0.0, "log": []}
skipped.append((n, why))
print(f"{n:12s} {'ПРОПУСК':8s} — {why}")
continue
ok, log, secs, to = run_task(n, TASKS) ok, log, secs, to = run_task(n, TASKS)
body = log[-1] body = log[-1] if log else ""
n_ok = body.count("ok ") n_ok = body.count("ok ")
n_bad = body.count("FAIL ") n_bad = body.count("FAIL ")
results[n] = { results[n] = {
"pass": ok, "checks_ok": n_ok, "checks_fail": n_bad, "pass": ok, "skipped": False, "checks_ok": n_ok, "checks_fail": n_bad,
"seconds": round(secs, 2), "timeout": to, "log": log, "seconds": round(secs, 2), "timeout": to, "log": log,
} }
print(f"{n:12s} {'PASS' if ok else 'FAIL':7s} {n_ok:<9d} {n_bad:<5d} {secs:8.2f}") print(f"{n:12s} {'PASS' if ok else 'FAIL':8s} {n_ok:<9d} {n_bad:<5d} {secs:8.2f}")
total = len(names) tried = [n for n in names if not results[n].get("skipped")]
done = sum(1 for n in names if results[n]["pass"]) done = sum(1 for n in tried if results[n]["pass"])
with open(os.path.join(HERE, "results_tasks.json"), "w", encoding="utf-8") as f: with open(os.path.join(HERE, "results_tasks.json"), "w", encoding="utf-8") as f:
json.dump({"passed": done, "total": total, "tasks": results}, f, json.dump({"passed": done, "total": len(tried), "skipped": len(skipped),
"run_on": sys.platform, "tasks": results}, f,
ensure_ascii=False, indent=2) ensure_ascii=False, indent=2)
print(f"\nИТОГ ЗАДАЧ: {done}/{total} PASS. Подробности: results_tasks.json") print(f"\nИТОГ ЗАДАЧ: {done}/{len(tried)} PASS. Подробности: results_tasks.json")
if skipped:
print(f"Пропущено (не запускались, это не провал): {len(skipped)} — "
+ "; ".join(f"{n}: {why}" for n, why in skipped))
if do_submit: if do_submit:
os.makedirs(SUB, exist_ok=True) os.makedirs(SUB, exist_ok=True)
@@ -156,7 +272,7 @@ def main():
os.chmod(os.path.join(SUB, fn), 0o644) os.chmod(os.path.join(SUB, fn), 0o644)
print(f"Ответы скопированы в {SUB}") print(f"Ответы скопированы в {SUB}")
print("Пакет: 01-09 диагностика, 10-13 добор по слабым доменам (см. WEEK7.md).") print("Пакет: 01-09 диагностика, 10-13 добор по слабым доменам (см. WEEK7.md).")
sys.exit(0 if done == total else 1) sys.exit(0 if done == len(tried) else 1)
if __name__ == "__main__": if __name__ == "__main__":
+3 -2
View File
@@ -6,7 +6,7 @@ import sys
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "crash.c") SRC = os.path.join(HERE, "crash.c")
BIN = os.path.join(HERE, "crash_checked") BIN = os.path.join(HERE, "crash_checked" + (".exe" if os.name == "nt" else ""))
failures = 0 failures = 0
@@ -20,7 +20,8 @@ def check(cond, name, extra=""):
def main(): def main():
r = subprocess.run(["gcc", "-g", "-O1", "-Wall", "-Wextra", cc = os.environ.get("CC", "gcc")
r = subprocess.run([cc, "-g", "-O1", "-Wall", "-Wextra",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-fsanitize=address,undefined", "-fno-omit-frame-pointer",
SRC, "-o", BIN], capture_output=True, text=True, timeout=180) SRC, "-o", BIN], capture_output=True, text=True, timeout=180)
check(r.returncode == 0, "сборка с ASAN/UBSAN прошла", r.stderr[:800]) check(r.returncode == 0, "сборка с ASAN/UBSAN прошла", r.stderr[:800])