Files
RustNotMC/tools/test_server_ops_feedback_parity.py
T
SashegDev 874b6f20c2 Клиент: начальный импорт кодовой базы
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
2026-10-29 22:04:28 +03:00

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}')