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:
@@ -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__':
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user