Files

280 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()