From 75a10f999920d70294db0e9f842ef78089fac3e0 Mon Sep 17 00:00:00 2001 From: Kodlo-chan Date: Wed, 23 Sep 2026 17:48:28 +0700 Subject: [PATCH] =?UTF-8?q?grade.py:=20=D0=BD=D0=B5=20=D0=BF=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B0=20=D0=BC=D0=B0=D1=88=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B1=D0=B5=D0=B7=20=D0=BA=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D0=B8=D0=BB=D1=8F=D1=82=D0=BE=D1=80=D0=B0=20=E2=80=94=20=D0=9F?= =?UTF-8?q?=D0=A0=D0=9E=D0=9F=D0=A3=D0=A1=D0=9A=20=D1=81=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D1=87=D0=B8=D0=BD=D0=BE=D0=B9,=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE-=D0=BE=D1=82=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=81=D0=B0=D0=BD=D0=B8=D1=82=D0=B0=D0=B9=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=B2,=20.exe=20=D0=BF=D0=BE=D0=B4=20Windows,=20WS?= =?UTF-8?q?L-=D0=BF=D0=BE=D0=B4=D1=81=D0=BA=D0=B0=D0=B7=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 +++++ diag/README.md | 4 +- diag/grade.py | 146 ++++++++++++++++++++++++++++++---- diag/tasks/09_gdb/test_gdb.py | 5 +- 4 files changed, 155 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 55f1f8f..d0413be 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,24 @@ - `grade.py` — автопроверка задач (ASAN/UBSAN, таймауты, PASS/FAIL + полный лог); - `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 diff --git a/diag/README.md b/diag/README.md index 48478ce..8f6841d 100644 --- a/diag/README.md +++ b/diag/README.md @@ -54,7 +54,9 @@ python3 facts_drill.py --domain algo # только алгоритмы 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`-хелперов, без копирования чужих решений, `answer.txt` в задаче 09 — обязательная часть (оценивается ход разбора). diff --git a/diag/grade.py b/diag/grade.py index c4caa5e..b108eb7 100644 --- a/diag/grade.py +++ b/diag/grade.py @@ -22,9 +22,90 @@ HERE = os.path.dirname(os.path.abspath(__file__)) TASKS = os.path.join(HERE, "tasks") SUB = os.path.join(HERE, "submissions") +WINDOWS = os.name == "nt" +LINUX = sys.platform.startswith("linux") + CXX = os.environ.get("CXX", "g++") +CC = os.environ.get("CC", "gcc") SAN = "-fsanitize=address,undefined -fno-omit-frame-pointer" 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 = { @@ -49,6 +130,9 @@ def sh(cmd, cwd, timeout): try: r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout) 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: 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 @@ -68,13 +152,14 @@ def run_task(name, root): extra = [] if FAST else ["-fsanitize=thread"] elif not FAST: 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) log.append("$ " + " ".join(cmd) + "\n" + out) if rc != 0: return False, log, secs, timed_out - rc, out, secs2, timed_out = sh(["./t_" + name], d, timeout) - log.append("$ ./t_" + name + "\n" + out) + rc, out, secs2, timed_out = sh([run_path(binname)], d, timeout) + log.append("$ " + run_path(binname) + "\n" + out) secs += secs2 if "unsupported VMA" in out and not FAST: # ThreadSanitizer не поддерживает этот размер адресного пространства @@ -84,8 +169,8 @@ def run_task(name, root): rc, out2, s, to = sh(cmd2, d, timeout) log.append("$ " + " ".join(cmd2) + "\n# TSan недоступен на этой платформе, откат на ASAN\n" + out2) if rc == 0: - rc, out2, s2, to = sh(["./t_" + name], d, timeout) - log.append("$ ./t_" + name + "\n" + out2) + rc, out2, s2, to = sh([run_path("t_" + name)], d, timeout) + log.append("$ " + run_path("t_" + name) + "\n" + out2) secs += s2 elif kind == "bin": # epoll-сервер: solution.cpp -> бинарь solution, затем python-тест @@ -110,7 +195,7 @@ def run_task(name, root): def main(): - global FAST + global FAST, CXX, CC, CXX_PATH, CC_PATH, HAS_SAN args = [a for a in sys.argv[1:]] FAST = "--fast" in args do_submit = "--submit" in args @@ -120,25 +205,56 @@ def main(): else: 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 = {} - print(f"{'задача':12s} {'итог':7s} {'проверок':9s} {'FAIL':5s} {'время':8s}") + skipped = [] + print(f"{'задача':12s} {'итог':8s} {'проверок':9s} {'FAIL':5s} {'время':8s}") 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) - body = log[-1] + body = log[-1] if log else "" n_ok = body.count("ok ") n_bad = body.count("FAIL ") 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, } - 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) - done = sum(1 for n in names if results[n]["pass"]) + tried = [n for n in names if not results[n].get("skipped")] + 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: - 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) - 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: os.makedirs(SUB, exist_ok=True) @@ -156,7 +272,7 @@ def main(): os.chmod(os.path.join(SUB, fn), 0o644) print(f"Ответы скопированы в {SUB}") 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__": diff --git a/diag/tasks/09_gdb/test_gdb.py b/diag/tasks/09_gdb/test_gdb.py index 860eced..578cd3b 100644 --- a/diag/tasks/09_gdb/test_gdb.py +++ b/diag/tasks/09_gdb/test_gdb.py @@ -6,7 +6,7 @@ import sys HERE = os.path.dirname(os.path.abspath(__file__)) 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 @@ -20,7 +20,8 @@ def check(cond, name, extra=""): 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", SRC, "-o", BIN], capture_output=True, text=True, timeout=180) check(r.returncode == 0, "сборка с ASAN/UBSAN прошла", r.stderr[:800])