Public Access
Complete 26.3 parity acceptance and dual-version release workflow
Build and smoke / changes (push) Waiting to run
Build and smoke / changes (push) Waiting to run
This commit is contained in:
1 parent
41daff1e61
commit
4a59fb5d02
26 files changed
+1241
-369
No files matched your search
@@ -0,0 +1,166 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user