#!/usr/bin/env python3 """Автопроверка практических задач диагностики. Использование (из каталога diag): python3 grade.py # проверить все задачи (с санитайзерами) python3 grade.py 01 04 07 # только указанные python3 grade.py --fast # без ASAN/UBSAN/TSAN (быстрее, слабее) python3 grade.py --submit # после проверки скопировать ответы в submissions/ Что требуется от решающего: файл solution.cpp / solution.sh / crash.c + answer.txt, каждый в своём каталоге tasks//. Результат: results_tasks.json (по задаче: pass, сколько проверок, время, лог). """ import json import os import shutil import subprocess import sys import time 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 = { "01_bits": ("cpp", "test_bits.cpp", 180), "02_list": ("cpp", "test_list.cpp", 180), "03_ring": ("cpp", "test_ring.cpp", 180), "04_ipv4": ("cpp", "test_ipv4.cpp", 180), "05_lis": ("cpp", "test_lis.cpp", 300), "06_threads": ("cpp", "test_threads.cpp", 600), # собирается с TSan, не с ASAN "07_epoll": ("bin", "test_epoll.py", 300), "08_bash": ("bash", "test_bash.py", 300), "09_gdb": ("py", "test_gdb.py", 300), "10_hash": ("cpp", "test_hash.cpp", 300), "11_heap": ("cpp", "test_heap.cpp", 600), "12_dijkstra":("cpp", "test_dijkstra.cpp",600), "13_subnet": ("cpp", "test_subnet.cpp", 120), } def sh(cmd, cwd, timeout): t0 = time.time() 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 def run_task(name, root): d = os.path.join(root, name) kind, test, timeout = SPEC[name] log = [] rc = None timed_out = False if kind == "cpp": flags = ["-std=c++17", "-O2", "-Wall", "-Wextra", "-g"] extra = [] if name == "06_threads": flags = ["-std=c++17", "-O1", "-Wall", "-Wextra", "-g", "-pthread"] extra = [] if FAST else ["-fsanitize=thread"] elif not FAST: extra = SAN.split() 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([run_path(binname)], d, timeout) log.append("$ " + run_path(binname) + "\n" + out) secs += secs2 if "unsupported VMA" in out and not FAST: # ThreadSanitizer не поддерживает этот размер адресного пространства # (Raspberry Pi / ядро 6.18, 47-бит VMA) — откатываемся на ASAN. cmd2 = [CXX, "-std=c++17", "-O1", "-Wall", "-Wextra", "-g", "-pthread"] \ + SAN.split() + [test, "-o", "t_" + name] 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([run_path("t_" + name)], d, timeout) log.append("$ " + run_path("t_" + name) + "\n" + out2) secs += s2 elif kind == "bin": # epoll-сервер: solution.cpp -> бинарь solution, затем python-тест cmd = [CXX, "-std=c++17", "-O2", "-Wall", "-Wextra", "-g", "solution.cpp", "-o", "solution"] 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([sys.executable, test], d, timeout) log.append("$ python3 " + test + "\n" + out) secs += secs2 else: # bash / py — тест сам всё делает if name == "08_bash": os.chmod(os.path.join(d, "solution.sh"), 0o755) rc, out, secs, timed_out = sh([sys.executable, test], d, timeout) log.append("$ python3 " + test + "\n" + out) body = log[-1] passed = (rc == 0) and ("FAIL" not in body) and ("ALL PASS" in body) return passed, log, secs, timed_out def main(): 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 names = [a for a in args if a.isdigit() or a in SPEC] if names: names = [n for n in SPEC if n.split("_")[0] in names or n in names] 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 = {} 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] if log else "" n_ok = body.count("ok ") n_bad = body.count("FAIL ") results[n] = { "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':8s} {n_ok:<9d} {n_bad:<5d} {secs:8.2f}") 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": len(tried), "skipped": len(skipped), "run_on": sys.platform, "tasks": results}, f, ensure_ascii=False, indent=2) 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) for fn in ("results_quiz.json", "results_tasks.json"): p = os.path.join(HERE, fn) if os.path.exists(p): shutil.copy2(p, os.path.join(SUB, fn)) for n in SPEC: d = os.path.join(TASKS, n) for fn in ("solution.cpp", "solution.sh", "crash.c", "answer.txt"): p = os.path.join(d, fn) if os.path.exists(p): shutil.copy2(p, os.path.join(SUB, f"{n}__{fn}")) for fn in os.listdir(SUB): os.chmod(os.path.join(SUB, fn), 0o644) print(f"Ответы скопированы в {SUB}") print("Пакет: 01-09 диагностика, 10-13 добор по слабым доменам (см. WEEK7.md).") sys.exit(0 if done == len(tried) else 1) if __name__ == "__main__": main()