#!/usr/bin/env python3 """Игровая проверка сдачи (клавиша G) и рейтинга в гонке: 2 клиента-бота на локальном сервере. tools/test/race_giveup_test.py [рабочая_папка=/tmp/race_giveup] Круг 1: A на воротах 1 держит G 1,0 с и отпускает (отмена), на воротах 2 сдаётся; B финиширует. Ожидание: итоги одинаковы у обоих (B 1-й со временем, A «сдался»), в рейтинг попал только B. Круг 2: A сдаётся на воротах 1, B на воротах 2 — все сдались, круг сразу заканчивается, в рейтинге по-прежнему только B. Порт 9080 должен быть свободен.""" import os, re, shutil, signal, subprocess, sys, time, json SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) W = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else "/tmp/race_giveup") XDG = os.path.join(W, "xdg") ok = True def check(cond, text): global ok print((" ДА " if cond else " НЕТ ") + text) if not cond: ok = False shutil.rmtree(W, ignore_errors=True); os.makedirs(W) subprocess.run([f"{SRC}/tools/test/make_test_server.sh", f"{W}/server"], check=True, stdout=subprocess.DEVNULL) subprocess.run([f"{SRC}/tools/test/make_test_project.sh", f"{W}/project"], check=True, stdout=subprocess.DEVNULL) pg = f"{W}/project/project.godot" t = open(pg).read() t = t.replace('Bot="*res://tools/test/bot.gd"', 'Bot="*res://tools/test/bot.gd"\nRaceBot="*res://tools/test/race_bot.gd"').replace('config/name="world2"', 'config/name="world2-racetest"') open(pg, "w").write(t) subprocess.run(["godot", "--headless", "--import", "--path", f"{W}/project"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for n, nick, cid in (("A", "Aa", "a" * 32), ("B", "Bb", "b" * 32)): open(f"{W}/prof_{n}.cfg", "w").write(f'[profile]\nclient_id="{cid}"\nnick="{nick}"\nasked=true\n') slog = open(f"{W}/server.log", "w") server = subprocess.Popen(["godot", "--headless", "--path", f"{W}/server"], stdout=slog, stderr=subprocess.STDOUT, env=dict(os.environ, XDG_DATA_HOME=XDG)) time.sleep(2.5) def bot(n, *args): return subprocess.Popen(["godot", "--headless", "--path", f"{W}/project", "res://maps/race/race.tscn", "--", "--client", "--race-log", f"--race-best={W}/best_{n}.cfg", f"--profile={W}/prof_{n}.cfg", f"--name={n}", "--expect=2", "--rounds=2", *args], stdout=open(f"{W}/{n}.log", "w"), stderr=subprocess.STDOUT) pa = bot("A", "--gu1=cancel@1,giveup@2", "--gu2=giveup@1") pb = bot("B", "--delay=2.0", "--gu2=giveup@2") try: pa.wait(timeout=420); pb.wait(timeout=60) except subprocess.TimeoutExpired: pa.kill(); pb.kill() check(False, "боты не уложились во время") time.sleep(1) server.terminate(); server.wait(); slog.close() L = {n: open(f"{W}/{n}.log", errors="replace").read().splitlines() for n in "AB"} sv = open(f"{W}/server.log", errors="replace").read() def tables(lines): res, cur = [], [] for l in lines: m = re.search(r"итог: место (\d+) игрок (-?\d+) время (-?[\d.]+) ворот (\d+) сдался=(\w+)", l) if m: if cur and int(m.group(1)) == 1: res.append(cur); cur = [] cur.append((int(m.group(1)), int(m.group(2)), float(m.group(3)), int(m.group(4)), m.group(5))) elif cur and "фаза -> WAIT" in l: res.append(cur); cur = [] if cur: res.append(cur) return res ta, tb = tables(L["A"]), tables(L["B"]) print("таблицы A:", ta); print("таблицы B:", tb) check(len(ta) == 2 and len(tb) == 2, "оба видели итоги двух кругов") check(ta == tb, "итоги одинаковы у A и B в обоих кругах (включая признак «сдался»)") if len(ta) == 2: r1 = ta[0] check(len(r1) == 2 and r1[0][2] > 0 and r1[0][4] == "false" and r1[1][4] == "true" and r1[1][2] < 0, "круг 1: первым финишировавший B со временем, вторым сдавшийся A без времени") check(len(set(x[1] for x in r1)) == 2 and r1[1][3] >= 2, "A сдался, пройдя %d ворот" % r1[1][3]) check(all(x[4] == "true" and x[2] < 0 for x in ta[1]) and len(ta[1]) == 2, "круг 2: оба сдались, времени ни у кого нет") a = "\n".join(L["A"]) check("ОТМЕНА_УДЕРЖАНИЯ: отпускаю на" in a and re.search(r"шкала=(0\.\d+|1\.[0-4]\d*) сдался=false", a) is not None, "отмена: G отпущена через 1,0 с (< 1,5 с) — не сдался") check(re.search(r"ПОСЛЕ_ОТМЕНЫ hold=0\.00 сдался=false", a) is not None, "после отмены шкала сброшена в 0") sd = re.findall(r"СДАЛСЯ_БОТ unix=([\d.]+) на_старте=(\w+) заморожен=(\w+)", a + "\n" + "\n".join(L["B"])) check(len(sd) >= 2 and all(x[1] == "true" and x[2] == "false" for x in sd), "после сдачи игрок на стартовой площадке и не заморожен (%d сдач; последний сдавшийся круга не успевает напечатать — круг кончается в тот же кадр)" % len(sd)) # круг 2: все сдались -> RESULTS в тот же кадр, а не через 45 с race_unix = {n: [float(m.group(1)) for l in L[n] for m in [re.search(r"ФАЗА RACE, unix=([\d.]+)", l)] if m] for n in "AB"} res_unix = {n: [float(m.group(1)) for l in L[n] for m in [re.search(r"ФАЗА RESULTS, unix=([\d.]+)", l)] if m] for n in "AB"} print(" unix RACE:", race_unix, " RESULTS:", res_unix) if len(res_unix["A"]) == 2 and len(race_unix["A"]) == 2: d = res_unix["A"][1] - race_unix["A"][1] check(d < 30.0, "круг 2 (все сдались) закончился через %.1f с после старта, не ждал 45 с" % d) imm = False for n in "AB": for k, l in enumerate(L[n]): if "СДАЛСЯ (время гонки" in l and any("фаза -> RESULTS" in x for x in L[n][k + 1:k + 4]): imm = True check(imm, "последняя сдача сразу привела к итогам (СДАЛСЯ -> фаза RESULTS подряд в логе)") rec = re.findall(r"\[рекорд\] race (\S+) ([\d.]+)", sv) print(" записи на сервере:", rec) check(len(rec) == 1 and rec[0][0] == "Bb", "в рейтинг попала одна запись — B (сдавшийся A не записан)") for n in "AB": errs = [l for l in L[n] if "SCRIPT ERROR" in l or "Parse Error" in l] check(not errs, "в логе %s нет ошибок скриптов %s" % (n, errs[:2])) print("\nПРОВЕРКА ПРОЙДЕНА" if ok else "\nПРОВЕРКА ПРОВАЛЕНА") sys.exit(0 if ok else 1)