280 lines
12 KiB
Python
280 lines
12 KiB
Python
#!/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/<NN_имя>/.
|
||
Результат: 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()
|