#!/usr/bin/env python3 # Предполётная проверка кампании: каждый сценарий запускается на два шага. # # python preflight.py # проверить все # python preflight.py --group I # только группу # # Ловит опечатки в ключах, несовместимые сочетания и пределы железа ДО того, как кампания # уйдёт считать на девяносто часов. Не бесплатная привычка, а окупившаяся: именно так # выяснилось, что вся группа сверхмелких сеток падала на пределе GPU в 65535 рабочих групп # на измерение, а девять прогонов группы F передавали --body-x дважды. # # Физику проверка не трогает вовсе: два шага — это ровно «запустилось и не упало». import argparse import json import os import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) BIN = os.environ.get("KBC2D_BIN") or os.path.join( HERE, "..", "target", "release", "kbc2d" + (".exe" if os.name == "nt" else "")) # Всё, что пишет файлы, выкидываем: проверяем запуск, а не вывод. Ключ идёт со значением, # поэтому следующий за ним аргумент тоже пропускается. DROP = {"--gif", "--xt", "--case-csv", "--gif-field", "--gif-downsample", "--gif-scale", "--gif-every", "--gif-fps", "--series-every"} # Строки, которые ничего не объясняют и только вытесняют полезные. NOISE = ( "note: run with `RUST_BACKTRACE", "note: Some details are omitted", "WARNING: dzn is not a conformant", "stack backtrace", "Caused by:", ) def explain(output): """Достать из вывода упавшего прогона то, ради чего его вообще читают. Раньше бралась последняя строка вывода — а у паники Rust последняя строка это «note: run with RUST_BACKTRACE=1», то есть ровно ноль сведений о причине. Полезное лежит либо в собственном сообщении решателя, либо на строке ПОСЛЕ «panicked at»: там текст самой паники. """ out = [l.rstrip() for l in output.splitlines() if l.strip()] out = [l for l in out if not any(n in l for n in NOISE)] if not out: return "(без вывода)" for l in out: if l.lstrip().startswith("ОШИБКА"): return l.strip() for i, l in enumerate(out): if "panicked at" in l: rest = [x.strip() for x in out[i + 1:]] return " | ".join(rest[:4]) if rest else l.strip() return out[-1].strip() def main(): ap = argparse.ArgumentParser() ap.add_argument("--group", help="только эта группа, например I") ap.add_argument("--scenarios", default=os.path.join(HERE, "scenarios.json")) args = ap.parse_args() runs = json.load(open(args.scenarios, encoding="utf-8"))["runs"] if args.group: runs = [r for r in runs if r["group"].upper() == args.group.upper()] if not runs: print("под выборку не попал ни один прогон") return 0 if not os.path.exists(BIN): print(f"не найден бинарь решателя: {BIN}") return 1 bad = [] for i, r in enumerate(runs, 1): argv, skip = [], False for a in r["args"]: if skip: skip = False continue if a in DROP: skip = True continue argv.append(a) if "--steps" in argv: argv[argv.index("--steps") + 1] = "2" else: argv += ["--steps", "2"] p = subprocess.run([BIN] + argv + ["--verbose", "quiet", "--report-every", "0"], capture_output=True, cwd=HERE) ok = p.returncode == 0 print("." if ok else "X", end="", flush=True) if not ok: out = (p.stdout + p.stderr).decode("utf-8", "replace") bad.append((r["id"], explain(out))) if i % 40 == 0: print(f" {i}/{len(runs)}", flush=True) print(f"\n\nпроверено сценариев: {len(runs)}, не запустились: {len(bad)}") for id_, msg in bad: print(f" {id_}") print(f" {msg}") return 1 if bad else 0 if __name__ == "__main__": sys.exit(main())