874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
164 lines
8.7 KiB
Python
164 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Command feedback the operators get on a dedicated server (`[Server: ...]`), against vanilla's.
|
|
|
|
CommandSourceStack.broadcastToAdmins: a command's success that vanilla sends with `broadcast` goes, gray and
|
|
italic as `[name: message]`, to every other operator while `send_command_feedback` is on, including the
|
|
console's own commands when `broadcast-console-to-ops` is (DedicatedServer.shouldInformAdmins).
|
|
|
|
A vanilla dedicated server and Horizon's (`horizon-client --dedicated --nogui`) each serve a copy of one flat
|
|
world vanilla generated first, with the same server.properties and Alice and Bob as operators. LocatorBarClient
|
|
joins them both (players made of vanilla's protocol code) and records the system chat each receives while, step
|
|
by step, the console sets the time, asks for the difficulty (a query, told to nobody), whitelists a player,
|
|
deops Bob (who stops hearing of it) and ops him again, turns `send_command_feedback` off and on and saves, and
|
|
Alice runs a command herself (Bob hears of it as an operator). Then again with `broadcast-console-to-ops=false`.
|
|
Each player's chat in each step is compared exactly.
|
|
"""
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
try:
|
|
from . import lab, test_rust_parity as base
|
|
from .test_dedicated_server_parity import Server, game_time
|
|
from .test_locator_bar_parity import Client, generate_world, server_dir
|
|
except ImportError:
|
|
import lab
|
|
import test_rust_parity as base
|
|
from test_dedicated_server_parity import Server, game_time
|
|
from test_locator_bar_parity import Client, generate_world, server_dir
|
|
|
|
NAME = 'server-ops-feedback-parity'
|
|
# (label, what the console does, what the players do).
|
|
BROADCAST = [
|
|
('console-time', ['time set 1000'], []),
|
|
('console-query', ['difficulty'], []),
|
|
('console-whitelist', ['whitelist add Alice'], []),
|
|
('console-deop', ['deop Bob'], []),
|
|
('bob-not-op', ['time set 2000'], []),
|
|
('alice-bob-not-op', [], ['cmd Alice time set 3000']),
|
|
('console-op', ['op Bob'], []),
|
|
('alice-bob-op', [], ['cmd Alice time set 4000']),
|
|
('feedback-off', ['gamerule send_command_feedback false'], []),
|
|
('console-feedback-off', ['time set 5000'], []),
|
|
('feedback-on', ['gamerule send_command_feedback true'], []),
|
|
('console-save', ['save-all'], []),
|
|
]
|
|
QUIET = [
|
|
('console-time', ['time set 1000'], []),
|
|
('console-whitelist', ['whitelist add Alice'], []),
|
|
('alice', [], ['cmd Alice time set 3000']),
|
|
]
|
|
CASES = {'broadcast': ({}, BROADCAST), 'quiet': ({'broadcast-console-to-ops': 'false'}, QUIET)}
|
|
STEP_TICKS = 40
|
|
# A step ends by the server's clock (Client.wait): after a round trip of Alice's (Bob is deopped for a while, so
|
|
# his is not used). While send_command_feedback is off a command has no reply for her, so those steps wait for
|
|
# the ticks alone.
|
|
NO_SYNC = {'feedback-off', 'console-feedback-off'}
|
|
# Steps whose reply can take longer than a step: the next step waits for this console line first. A loaded machine
|
|
# took 2 s to save vanilla's world, and the transcript ended before "Saved the game".
|
|
WAIT_FOR = {'console-save': r'System chat: Saved the game'}
|
|
|
|
|
|
def run_side(label, case, server_command, client_command, build, world):
|
|
properties, script = CASES[case]
|
|
directory = build / label / case
|
|
directory.parent.mkdir(parents=True, exist_ok=True)
|
|
port = server_dir(directory, world)
|
|
with (directory / 'server.properties').open('a', encoding='iso-8859-1') as f:
|
|
f.writelines(f'{k}={v}\n' for k, v in properties.items())
|
|
out = build / f'{label}-{case}.txt'
|
|
out.unlink(missing_ok=True)
|
|
server = Server(server_command, directory, f'{label}-{case}')
|
|
try:
|
|
server.wait_for(r'Done \(')
|
|
with (build / f'{label}-{case}-client.log').open('w') as log:
|
|
client = Client(client_command, port, out, log)
|
|
try:
|
|
for name in ('Alice', 'Bob'):
|
|
client.tell(f'join {name}')
|
|
server.wait_for(rf'{name} joined the game', timeout=120)
|
|
client.wait(server, ['Alice'], STEP_TICKS)
|
|
client.tell('chat on')
|
|
for step, console, actions in script:
|
|
client.tell(f'step {step}')
|
|
for command in console:
|
|
server.console(command)
|
|
game_time(server) # the console runs its queue in order: these commands have run
|
|
for action in actions:
|
|
client.tell(action)
|
|
if step in WAIT_FOR:
|
|
server.wait_for(WAIT_FOR[step], timeout=120)
|
|
client.wait(server, [] if step in NO_SYNC else ['Alice'], STEP_TICKS)
|
|
client.end()
|
|
finally:
|
|
client.kill()
|
|
finally:
|
|
server.stop()
|
|
if not out.exists():
|
|
raise AssertionError(f'{label} {case}: the client wrote no transcript (see {build}/{label}-{case}-client.log)')
|
|
lines = out.read_text(encoding='utf-8').splitlines()
|
|
errors = [l for l in lines if ' error ' in f' {l} ' or 'disconnected' in l]
|
|
return [f'{case} {l}' for l in lines if l.split(' ', 2)[2:3] and l.split(' ', 2)[2].startswith('chat ')], errors
|
|
|
|
|
|
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', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client'], cwd=lab.ROOT,
|
|
check=True, capture_output=True)
|
|
import json
|
|
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'],
|
|
cwd=lab.ROOT, text=True))
|
|
binary = Path(metadata['target_directory']) / 'fast' / ('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 = generate_world(vanilla_server, build)
|
|
sides = {'vanilla': ([], []), 'horizon': ([], [])}
|
|
for case in CASES:
|
|
for label, command in [('vanilla', vanilla_server), ('horizon', horizon_server)]:
|
|
lines, errors = run_side(label, case, command, client, build, world)
|
|
sides[label][0].extend(lines)
|
|
sides[label][1].extend(errors)
|
|
(vanilla, vanilla_errors), (horizon, horizon_errors) = sides['vanilla'], sides['horizon']
|
|
(build / 'vanilla.txt').write_text('\n'.join(vanilla) + '\n', encoding='utf-8')
|
|
(build / 'horizon.txt').write_text('\n'.join(horizon) + '\n', encoding='utf-8')
|
|
errors = vanilla_errors + horizon_errors
|
|
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 sum('[Server' in l or 'chat.type.admin' in l for l in vanilla) < 5:
|
|
raise AssertionError(f'vanilla sent the operators too little: the script did not run (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='The system chat two operators get on a dedicated server while the console and one of them run '
|
|
'commands: broadcastToAdmins for the console (broadcast-console-to-ops on and off) and a player, '
|
|
'queries, deop and op, send_command_feedback off and on, and save-all.'))
|
|
print(f'server ops feedback: {len(vanilla)} chat lines identical')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main()
|
|
except AssertionError as error:
|
|
sys.exit(f'{NAME}: {error}')
|