Harnesses: one scaffold for one-player dedicated server scripts

test_hardcore_respawn_parity.py copied test_locator_bar_parity.py's
build and run code; both now share locator.sides, and one-player
scripts run through locator.one_player.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ludovic Marie
2026-10-25 17:26:43 +01:00
parent 0481dbdb81
commit 50727eb5c3
2 changed files with 84 additions and 106 deletions
+12 -102
View File
@@ -8,23 +8,12 @@ vanilla's protocol code) joins, runs /kill on itself and presses the death scree
respawns the player, then makes it a spectator because the server is hardcore. The game mode changes and respawns
Alice is sent in the death and respawn steps are compared exactly.
"""
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import time
try:
from . import lab, test_rust_parity as base, test_locator_bar_parity as locator
from .test_dedicated_server_parity import Server
from . import test_locator_bar_parity as locator
except ImportError:
import lab
import test_rust_parity as base
import test_locator_bar_parity as locator
from test_dedicated_server_parity import Server
NAME = 'hardcore-respawn-parity'
# server_dir and generate_world write these server.properties.
@@ -33,103 +22,24 @@ SCRIPT = [
('death', ['cmd Alice kill @s']),
('respawn', ['respawn Alice']),
]
STEP_SECONDS = 4.0
SHOWN = ('gamemode', 'respawn')
def run_side(label, server_command, client_command, build, world):
directory = build / label
port = locator.server_dir(directory, world)
out = build / f'{label}.txt'
out.unlink(missing_ok=True)
server = Server(server_command, directory, label)
try:
server.wait_for(r'Done \(')
with (build / f'{label}-client.log').open('w') as log:
client = subprocess.Popen([*map(str, client_command), '127.0.0.1', str(port), str(out)],
cwd=lab.ROOT, stdin=subprocess.PIPE, stdout=log, stderr=subprocess.STDOUT,
text=True)
def tell(line):
client.stdin.write(line + '\n')
client.stdin.flush()
try:
tell('players on')
tell('state on')
tell('step joins')
tell('join Alice')
server.wait_for(r'Alice joined the game', timeout=120)
time.sleep(STEP_SECONDS)
for step, actions in SCRIPT:
tell(f'step {step}')
for action in actions:
tell(action)
time.sleep(STEP_SECONDS)
tell('end')
client.wait(timeout=60)
finally:
if client.poll() is None:
client.kill()
finally:
server.stop()
if not out.exists():
raise AssertionError(f'{label}: the client wrote no transcript (see {build}/{label}-client.log)')
return out.read_text(encoding='utf-8').splitlines()
def shown(lines):
"""Alice's game mode changes and respawns in the death and respawn steps."""
keep = []
for line in lines:
parts = line.split(' ', 3)
if len(parts) >= 3 and parts[1] in ('step=death', 'step=respawn') and parts[2] in SHOWN:
keep.append(line)
return keep
return [line for line in lines if len(parts := line.split(' ', 3)) >= 3
and parts[1] in ('step=death', 'step=respawn') and parts[2] in ('gamemode', 'respawn')]
def check(vanilla, _):
if 'Alice step=respawn gamemode 3' not in vanilla:
return 'vanilla did not make the respawned player a spectator'
return None
def main():
lock, jars, libraries = base.verify_artifacts()
build = lab.LOCAL / NAME
build.mkdir(parents=True, exist_ok=True)
report = build / 'report.json'
report.unlink(missing_ok=True)
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java'
classes = build / 'classes'
shutil.rmtree(classes, ignore_errors=True)
classes.mkdir()
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-nowarn', '-sourcepath',
lab.ROOT / 'mods/horizon/src/test/java', '-cp', dependencies, '-d', classes, oracle],
cwd=lab.ROOT, check=True, capture_output=True)
subprocess.run(['cargo', 'build', '--release', '-p', 'horizon-client', '--bin', 'horizon-client'], cwd=lab.ROOT,
check=True, capture_output=True)
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'],
cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'release' / ('horizon-client.exe' if os.name == 'nt' else 'horizon-client')
vanilla_server = ['java', '-Xmx2g', '-Duser.timezone=UTC', '-cp', dependencies, 'net.minecraft.server.Main', 'nogui']
horizon_server = [binary, '--resources', lab.LOCAL / 'horizon-client/26.3', '--dedicated', '--nogui']
client = ['java', '-Xmx1g', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.LocatorBarClient']
world = locator.generate_world(vanilla_server, build)
vanilla_all = run_side('vanilla', vanilla_server, client, build, world)
horizon_all = run_side('horizon', horizon_server, client, build, world)
errors = [l for l in vanilla_all + horizon_all if ' error ' in f' {l} ' or 'disconnected' in l]
vanilla, horizon = shown(vanilla_all), shown(horizon_all)
if errors or vanilla != horizon:
import difflib
diff = '\n'.join(difflib.unified_diff(vanilla, horizon, 'vanilla', 'horizon', n=1, lineterm=''))
raise AssertionError(f'{len(errors)} errors, transcripts differ (see {build}/vanilla.txt and horizon.txt):\n'
+ '\n'.join(errors[:10]) + '\n' + diff[:6000])
if 'Alice step=respawn gamemode 3' not in vanilla:
raise AssertionError(f'vanilla did not make the respawned player a spectator: the script did not run '
f'(see {build})')
lab.write_json(report, dict(
schema=1, minecraft=lock['minecraft']['version'], platform=platform.platform(),
java=subprocess.check_output(['java', '--version'], text=True).splitlines()[0],
lines=len(vanilla), output_sha256=lab.digest(build / 'horizon.txt', 'sha256'),
scope='A player dying and respawning in a hardcore world on a dedicated server: the respawn and the game '
'mode it is sent (spectator).'))
print(f'hardcore respawn: {len(vanilla)} lines identical')
lines = locator.one_player(NAME, SCRIPT, shown, check, 'A player dying and respawning in a hardcore world on a '
'dedicated server: the respawn and the game mode it is sent (spectator).')
print(f'hardcore respawn: {len(lines)} lines identical')
if __name__ == '__main__':
+72 -4
View File
@@ -205,12 +205,13 @@ def run_side(label, server_command, client_command, build, world):
return out.read_text(encoding='utf-8').splitlines()
def main():
def sides(name):
"""The vanilla and Horizon dedicated servers and the LocatorBarClient that joins them, built for harness
`name`: (lock, build directory, vanilla server, Horizon server, client) commands."""
lock, jars, libraries = base.verify_artifacts()
build = lab.LOCAL / NAME
build = lab.LOCAL / name
build.mkdir(parents=True, exist_ok=True)
report = build / 'report.json'
report.unlink(missing_ok=True)
(build / 'report.json').unlink(missing_ok=True)
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java'
classes = build / 'classes'
shutil.rmtree(classes, ignore_errors=True)
@@ -228,7 +229,74 @@ def main():
vanilla_server = ['java', '-Xmx2g', '-Duser.timezone=UTC', '-cp', dependencies, 'net.minecraft.server.Main', 'nogui']
horizon_server = [binary, '--resources', lab.LOCAL / 'horizon-client/26.3', '--dedicated', '--nogui']
client = ['java', '-Xmx1g', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.LocatorBarClient']
return lock, build, vanilla_server, horizon_server, client
def one_player(name, script, shown, check, scope, step_seconds=4.0):
"""A harness of one player, Alice: on each server she joins, then runs `script`, a list of (step, client
actions) `step_seconds` apart. The lines `shown(transcript)` keeps must match vanilla's exactly, and
`check(vanilla's shown lines, vanilla's transcript)` returns why vanilla's run is no proof (the script did
not run), or None."""
lock, build, vanilla_server, horizon_server, client = sides(name)
def run(label, server_command, world):
directory = build / label
port = server_dir(directory, world)
out = build / f'{label}.txt'
out.unlink(missing_ok=True)
server = Server(server_command, directory, label)
try:
server.wait_for(r'Done \(')
with (build / f'{label}-client.log').open('w') as log:
process = subprocess.Popen([*map(str, client), '127.0.0.1', str(port), str(out)], cwd=lab.ROOT,
stdin=subprocess.PIPE, stdout=log, stderr=subprocess.STDOUT, text=True)
def tell(line):
process.stdin.write(line + '\n')
process.stdin.flush()
try:
for line in ['players on', 'state on', 'step joins', 'join Alice']:
tell(line)
server.wait_for(r'Alice joined the game', timeout=120)
time.sleep(step_seconds)
for step, actions in script:
tell(f'step {step}')
for action in actions:
tell(action)
time.sleep(step_seconds)
tell('end')
process.wait(timeout=60)
finally:
if process.poll() is None:
process.kill()
finally:
server.stop()
if not out.exists():
raise AssertionError(f'{label}: the client wrote no transcript (see {build}/{label}-client.log)')
return out.read_text(encoding='utf-8').splitlines()
world = generate_world(vanilla_server, build)
vanilla_all = run('vanilla', vanilla_server, world)
horizon_all = run('horizon', horizon_server, world)
errors = [l for l in vanilla_all + horizon_all if ' error ' in f' {l} ' or 'disconnected' in l]
vanilla, horizon = shown(vanilla_all), shown(horizon_all)
if errors or vanilla != horizon:
import difflib
diff = '\n'.join(difflib.unified_diff(vanilla, horizon, 'vanilla', 'horizon', n=1, lineterm=''))
raise AssertionError(f'{len(errors)} errors, transcripts differ (see {build}/vanilla.txt and horizon.txt):\n'
+ '\n'.join(errors[:10]) + '\n' + diff[:6000])
if (why := check(vanilla, vanilla_all)) is not None:
raise AssertionError(f'{why}: the script did not run (see {build})')
lab.write_json(build / 'report.json', dict(
schema=1, minecraft=lock['minecraft']['version'], platform=platform.platform(),
java=subprocess.check_output(['java', '--version'], text=True).splitlines()[0],
lines=len(vanilla), output_sha256=lab.digest(build / 'horizon.txt', 'sha256'), scope=scope))
return vanilla
def main():
lock, build, vanilla_server, horizon_server, client = sides(NAME)
report = build / 'report.json'
world = generate_world(vanilla_server, build)
vanilla = run_side('vanilla', vanilla_server, client, build, world)
horizon = run_side('horizon', horizon_server, client, build, world)