"""The local test server in build/server, shared by the smoke test and the world builder: install, configure, start, talk to over RCON, stop.""" import contextlib import hashlib import json import re import secrets import shutil import socket import struct import subprocess import sys import time import tomllib import urllib.request from pathlib import Path from packwiz_sync import java, sync ROOT = Path(__file__).resolve().parent.parent BUILD = ROOT / "build" SERVER = BUILD / "server" CACHE = BUILD / ".cache" PACK = tomllib.loads((ROOT / "pack/pack.toml").read_text()) class Rcon: """Minimal Source RCON client (the protocol Minecraft speaks).""" def __init__(self, port, password): self.sock = socket.create_connection(("127.0.0.1", port), timeout=120) self.next_id = 0 self._send(3, password) if self._recv()[0] == -1: raise RuntimeError("RCON authentication failed") def _send(self, kind, body): self.next_id += 1 payload = struct.pack(" 10: polled = time.time() if "no tasks running" in self.command("chunky progress").lower() and self.count(done) <= before: raise RuntimeError("Chunky stopped without finishing the task") if report_every and time.time() - last > report_every: progress = re.findall(r"Task running for minecraft:overworld\. (.*)", self.log.read_text(errors="replace")) print(f" {time.time() - started:5.0f}s {progress[-1] if progress else 'starting'}") last = time.time() time.sleep(1) return time.time() - started def run_checks(self, checks_file=ROOT / "tools/smoke_checks.json"): """Run the RCON checks from tools/smoke_checks.json; returns the failures.""" failures = [] for check in json.loads(checks_file.read_text()): for cmd in check.get("setup", []): self.command(cmd) reply = self.command(check["command"]) for cmd in check.get("cleanup", []): self.command(cmd) ok = re.search(check["expect"], reply) is not None label = check.get("name", "/" + check["command"]) print(f" [{'ok' if ok else 'FAIL'}] {label}: {reply[:200]!r}") if not ok: failures.append(f"{label}: {check['command']!r} did not match {check['expect']!r}") return failures def run_selftest(self, timeout=300): """Run the companion mod's in-game tests (needs SELFTEST_JVM_ARGS); returns the failures.""" reply = self.command("cobblesurvival selftest run").strip() if "started" not in reply: return [f"selftest did not start: {reply!r}"] print(f" {reply}") deadline = time.time() + timeout while time.time() < deadline: time.sleep(2) status = self.command("cobblesurvival selftest status").strip() if status.startswith("selftest finished"): for line in re.findall(r"selftest (PASS \S+|FAIL .*)", self.log.read_text(errors="replace")): print(f" [{'ok' if line.startswith('PASS') else 'FAIL'}] {line[5:]}") return [] if "FAILED" not in status else [status] if self.proc.poll() is not None: return [f"server stopped during the selftest; see {self.log}"] return [f"selftest did not finish in {timeout}s: {self.command('cobblesurvival selftest status').strip()}"] def stop(self, timeout=300): if self.rcon: self.rcon.command("stop") self.rcon.close() self.rcon = None self.proc.wait(timeout=timeout) def __exit__(self, *exc): if self.rcon: self.rcon.close() if self.proc and self.proc.poll() is None: self.proc.kill() self.proc.wait()