grade.py: не падать на машине без компилятора — ПРОПУСК с причиной, авто-отключение санитайзеров, .exe под Windows, WSL-подсказка
This commit is contained in:
@@ -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
@@ -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
@@ -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__":
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
Reference in New Issue
Block a user