Public Access
167 lines
7.3 KiB
Python
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()
|