Files
rivencraft/scripts/regression_multiplayer_263.py
T
2026-11-14 12:17:49 -05:00

167 lines
7.3 KiB
Python

#!/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()