#!/usr/bin/env python3 """Two real 26.3 clients, one sequential server, normal book travel and full restart.""" from server_test_lock import server_test_lock import json import os from pathlib import Path import shutil import signal import socket import subprocess import tempfile import time ROOT = Path(__file__).resolve().parents[1] LOGS = ROOT / "build/verification/multiplayer263" class Handoff(RuntimeError): pass def wait_for(processes, log, marker, timeout=60): last_progress = time.monotonic() last_stage = "" while True: text = log.read_text(errors="replace") if log.exists() else "" if marker in text: return if "MC263_MATRIX FAIL" in text: raise RuntimeError(f"Multiplayer assertion failed; inspect {log}") stages = [line for line in text.splitlines() if "MC263_MATRIX PHASE" in line] if stages and stages[-1] != last_stage: last_stage = stages[-1] last_progress = time.monotonic() print(last_stage, flush=True) if any(process.poll() is not None for process in processes): raise RuntimeError(f"Process exited before {marker}; inspect {LOGS}") if time.monotonic() - last_progress >= timeout: raise Handoff(f"Stalled at {last_stage or marker}; stop and hand off, do not retry") time.sleep(.25) def stop(process, server=False): if process is None or process.poll() is not None: return if server: try: process.stdin.write("stop\n") process.stdin.flush() process.wait(timeout=25) return except (BrokenPipeError, subprocess.TimeoutExpired): pass os.killpg(process.pid, signal.SIGTERM) try: process.wait(timeout=10) except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL) process.wait() def run(): if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): raise SystemExit("This real-client matrix needs a graphics session. Use the manual checklist.") LOGS.mkdir(parents=True, exist_ok=True) work = Path(tempfile.mkdtemp(prefix="rivencraft-multiplayer263-")) server = work / "server" server.mkdir() with socket.socket() as reservation: reservation.bind(("127.0.0.1", 0)) port = reservation.getsockname()[1] settings = {"layers": [{"height": 1, "block": "minecraft:bedrock"}, {"height": 2, "block": "minecraft:dirt"}, {"height": 1, "block": "minecraft:grass_block"}], "biome": "minecraft:plains"} (server / "eula.txt").write_text("eula=true\n") (server / "server.properties").write_text( f"server-ip=127.0.0.1\nserver-port={port}\nonline-mode=false\nwhite-list=false\n" "view-distance=2\nsimulation-distance=2\ngenerate-structures=false\n" "level-type=minecraft:flat\nlevel-seed=263100\ngenerator-settings=" + json.dumps(settings) + "\n") for name in ("One", "Two"): client = work / f"client-{name}" (client / "config").mkdir(parents=True) (client / "config/neoforge-client.toml").write_text("showLoadWarnings=false\n") (client / "options.txt").write_text("renderDistance:2\nsimulationDistance:5\nmaxFps:25\npauseOnLostFocus:false\n") init = work / "fixture.gradle" init.write_text(""" gradle.beforeProject { p -> p.layout.buildDirectory.set(p.file(System.getenv('RIVENCRAFT_MATRIX_WORK') + '/build')) p.afterEvaluate { p.sourceSets.main.java.srcDir(p.file('""" + str(ROOT / "scripts/fixtures/mc263_multiplayer") + """')) p.neoForge.runs.client.gameDirectory = p.file(System.getenv('RIVENCRAFT_MATRIX_WORK') + '/client-' + p.findProperty('matrixClient')) p.neoForge.runs.client.programArgument('--width') p.neoForge.runs.client.programArgument('640') p.neoForge.runs.client.programArgument('--height') p.neoForge.runs.client.programArgument('480') } } """) env = dict(os.environ, RIVENCRAFT_MATRIX_WORK=str(work)) base = [str(ROOT / "gradlew"), "-p", str(ROOT / "ports/mc263"), "-I", str(init), "--no-daemon", "--console=plain"] server_process = None clients = [] handles = [] try: # Build fixture classes once in an isolated output tree. Shipping outputs # are never overwritten or packaged with these extra test channels. with (LOGS / "compile.log").open("w") as output: subprocess.run(base + ["classes"], cwd=ROOT, env=env, stdout=output, stderr=subprocess.STDOUT, check=True, timeout=300) for phase in ("first", "restart"): log = LOGS / f"server-{phase}.log" output = log.open("w") handles.append(output) server_process = subprocess.Popen(base + ["runServer", f"-PserverRunDir={server}"], cwd=ROOT, env=env, stdin=subprocess.PIPE, stdout=output, stderr=subprocess.STDOUT, text=True, start_new_session=True) wait_for([server_process], log, "Done (") clients = [] for name in ("One", "Two"): output = (LOGS / f"client-{name}-{phase}.log").open("w") handles.append(output) clients.append(subprocess.Popen(base + ["runClient", f"-PmatrixClient={name}", f"-PclientName=Matrix{name}", f"-PconnectAddress=127.0.0.1:{port}"], cwd=ROOT, env=env, stdout=output, stderr=subprocess.STDOUT, text=True, start_new_session=True)) wait_for([server_process], log, "MC263_MATRIX " + ("FIRST_PASS" if phase == "first" else "RESTART_PASS")) for process in clients: process.wait(timeout=30) if process.returncode: raise RuntimeError("Client exited unsuccessfully; inspect client log") clients.clear() stop(server_process, server=True) if server_process.returncode: raise RuntimeError("Server exited unsuccessfully") server_process = None print(f"PASS two-client {phase}", flush=True) except Handoff as error: directions = ( f"{error}\n\nManual test: two clients on one 26.3 server. Create a new Age while both " "are online; both visit via Descriptive Books and return via Linking Books. " "Create a second Age with different sky/lighting/weather. Restart and reconnect both, " "confirm names, seeds and return links. Refuse occupied unload, then vacate and " "unload/reload/reseed/retire an empty Age. A retired book must leave the player at source.\n" "Temporary worlds were removed; do not rerun this stalled test automatically.\n") (LOGS / "HANDOFF.md").write_text(directions) print("HANDOFF: " + directions, flush=True) raise SystemExit(2) finally: for process in clients: stop(process) stop(server_process, server=True) for handle in handles: handle.close() shutil.rmtree(work) print(f"Removed all temporary client/server worlds and fixture outputs; logs: {LOGS}", flush=True) def main(): with server_test_lock(): run() if __name__ == "__main__": main()