874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
136 lines
6.2 KiB
Python
136 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Mods' quiet commands on Horizon's dedicated server, end to end (horizon_protocol::quiet).
|
|
|
|
Horizon's dedicated server (a flat world) and Horizon's headless client (`HORIZON_NET_REPORT`, the net
|
|
harness), which joins, then sends a batch of quiet commands as a mod would (`HORIZON_NET_QUIET`):
|
|
|
|
- as a player who isn't an operator: `setblock` is refused (the block stays air), as it would be typed;
|
|
- as an operator: `setblock` runs, a `fill` that changes nothing fails, and `say` is refused (a chat
|
|
message needs the player's signature);
|
|
- either way the player's chat gets nothing of them, and the server logs one line per batch: the player,
|
|
the mod, how many ran and how many failed.
|
|
|
|
python3 tools/quiet_commands_e2e.py --bin-dir target/fast
|
|
|
|
The client runs with fresh HORIZON_HOME, launcher and XDG folders and without audio; the logs go to --out.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import lab # noqa: E402
|
|
from integration_env import run_game # noqa: E402
|
|
from test_dedicated_server_parity import Server # noqa: E402
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PLAYER = 'Tester'
|
|
PROPERTIES = {
|
|
'online-mode': 'false',
|
|
'white-list': 'false',
|
|
'level-type': 'minecraft\\:flat',
|
|
'generate-structures': 'false',
|
|
'spawn-protection': '0',
|
|
'view-distance': '3',
|
|
'simulation-distance': '3',
|
|
'pause-when-empty-seconds': '-1',
|
|
}
|
|
BLOCK = '2 -55 2'
|
|
# What a player would see of these commands, were they typed.
|
|
FEEDBACK = ['Changed the block', 'No blocks were filled', 'Unknown or incomplete command', 'hi from a mod',
|
|
'Could not set the block']
|
|
|
|
|
|
def join(binary, port, out, label, lines):
|
|
"""The headless client joins, sends `lines` as one quiet batch and leaves: its report."""
|
|
home = Path(tempfile.mkdtemp(prefix=f'quiet-{label}-'))
|
|
report = out / f'{label}-report.json'
|
|
# run_game gives the game fresh home, launcher and XDG folders.
|
|
env = dict(HORIZON_NET_REPORT=str(report), HORIZON_NET_QUIET=';'.join(lines))
|
|
with (out / f'{label}-client.log').open('w') as log:
|
|
run_game([str(binary), '--resources', str(lab.RESOURCES), '--render-distance', '2',
|
|
'--server', f'127.0.0.1:{port}', '--name', PLAYER],
|
|
cwd=home, env=dict(os.environ, **env), stdout=log, stderr=subprocess.STDOUT, timeout=300,
|
|
check=True)
|
|
shutil.rmtree(home, ignore_errors=True)
|
|
return json.loads(report.read_text())
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument('--bin-dir', help='a prebuilt horizon-client (default: build it with the fast profile)')
|
|
parser.add_argument('--out', default=str(ROOT / '.local/quiet-commands-e2e'), help='logs go here')
|
|
args = parser.parse_args()
|
|
out = Path(args.out)
|
|
shutil.rmtree(out, ignore_errors=True)
|
|
out.mkdir(parents=True)
|
|
if args.bin_dir:
|
|
binary = Path(args.bin_dir) / 'horizon-client'
|
|
else:
|
|
subprocess.run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client'],
|
|
cwd=ROOT, check=True)
|
|
binary = Path(lab.cargo_target()) / 'fast' / 'horizon-client'
|
|
port = lab.free_port()
|
|
directory = out / 'server'
|
|
directory.mkdir()
|
|
props = dict(PROPERTIES, **{'server-port': str(port)})
|
|
(directory / 'server.properties').write_text(''.join(f'{k}={v}\n' for k, v in props.items()),
|
|
encoding='iso-8859-1')
|
|
(directory / 'eula.txt').write_text('eula=true\n')
|
|
server = Server([binary, '--resources', lab.RESOURCES, '--dedicated', '--nogui'], directory, 'server')
|
|
failures = []
|
|
|
|
def check(what, ok, detail=''):
|
|
print(('PASS ' if ok else 'FAIL ') + what + ('' if ok else f': {detail}'))
|
|
if not ok:
|
|
failures.append(what)
|
|
|
|
def test_block(want):
|
|
"""The console's `execute if block`: 'Test passed' when the block is `want`."""
|
|
before = len(server.lines)
|
|
server.console(f'execute if block {BLOCK} {want}')
|
|
deadline = time.time() + 30
|
|
while time.time() < deadline:
|
|
answer = [l for l in server.lines[before:] if 'Test passed' in l or 'Test failed' in l]
|
|
if answer:
|
|
return 'Test passed' in answer[0]
|
|
time.sleep(0.2)
|
|
raise AssertionError('the console never answered execute if block')
|
|
|
|
try:
|
|
server.wait_for(r'Done \(')
|
|
# Not an operator: the setblock is refused, as typed.
|
|
got = join(binary, port, out, 'not-op', [f'setblock {BLOCK} minecraft:gold_block'])
|
|
server.wait_for(rf'{PLAYER} ran 1 quiet command\(s\) from mod net_harness: 1 failed', timeout=30)
|
|
check('not an operator: connected throughout', got['error'] is None, got['error'])
|
|
check("not an operator: a quiet setblock is refused", not test_block('minecraft:gold_block'))
|
|
heard = [c for c in got['chat'] if any(f in c for f in FEEDBACK)]
|
|
check('not an operator: nothing in the chat', not heard, heard)
|
|
server.wait_for(rf'{PLAYER} left the game', timeout=60)
|
|
# An operator: it runs, quietly.
|
|
server.console(f'op {PLAYER}')
|
|
server.wait_for(rf'Made {PLAYER} a server operator', timeout=30)
|
|
got = join(binary, port, out, 'op', [f'setblock {BLOCK} minecraft:gold_block',
|
|
f'fill {BLOCK} {BLOCK} minecraft:gold_block', 'say hi from a mod'])
|
|
server.wait_for(rf'{PLAYER} ran 3 quiet command\(s\) from mod net_harness: 2 failed', timeout=30)
|
|
check('operator: connected throughout', got['error'] is None, got['error'])
|
|
check('operator: the quiet setblock ran', test_block('minecraft:gold_block'))
|
|
heard = [c for c in got['chat'] if any(f in c for f in FEEDBACK)]
|
|
check('operator: nothing in the chat (no feedback, no failure, no say)', not heard, heard)
|
|
said = [l for l in server.lines if 'hi from a mod' in l]
|
|
check('operator: say is refused', not said, said)
|
|
finally:
|
|
server.stop()
|
|
print(f'logs: {out}')
|
|
sys.exit(1 if failures else 0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|