mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
The regression behind #9088 was #8245: a stop that doesn't come from the supervisor's stdin or the control API left the supervisor in its stopped phase, so `horizon-server --dir … run` outlived the management `server/stop` and the harness's `wait(timeout=180)` raised TimeoutExpired. #9077 fixed that (`Shared::shut_down`), on the base of this branch, and the harness passes here again. What the harness still left open, and what this fixes: - It could not run on Windows at all. It wrote the absolute keystore path into vanilla's server.properties with single backslashes, which Java's properties parser reads as escapes ("C:UserscocoAppData…"), so vanilla stopped before the first request. Values are now escaped as vanilla writes them, and the harness runs here. - A server that stays up after `server/stop` was reported as a bare TimeoutExpired. `wait_for_exit` waits for the process to end, which is the condition the issue was about (vanilla's `ServerStateService.stop` submits `MinecraftServer.halt(false)`, so `runServer` returns and `main` ends the process), and its failure names the side and the last lines it logged. The seconds each server took are kept in report.json: vanilla 3.4 s and Horizon 0.5 s here, against the 180 s budget. Fixes #9088 Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
945 lines
68 KiB
Python
945 lines
68 KiB
Python
#!/usr/bin/env python3
|
|
"""Vanilla's server management protocol (JSON-RPC over a WebSocket), Horizon's against vanilla's.
|
|
|
|
The pinned vanilla server (`net.minecraft.server.Main nogui`) and Horizon's (`horizon-server run`, which
|
|
serves the protocol beside the game it supervises) start in folders with the same lists (ops.json,
|
|
whitelist.json, banned-players.json, banned-ips.json) and the same `management-server-*` settings (a fixed
|
|
secret, one allowed origin, and TLS, vanilla's default, with a PKCS12 keystore `keytool` makes). Each is then
|
|
sent the same things over TLS, and every answer is compared exactly:
|
|
|
|
- HTTP requests without the secret, with a wrong one, and with it in the WebSocket subprotocol from an
|
|
origin that isn't allowed: the status and the JSON body;
|
|
- the handshake with the secret in the subprotocol from the allowed origin: the subprotocol answered;
|
|
- over a WebSocket with the bearer token, a sequence of messages: malformed JSON, a bare number, unknown and
|
|
unparsable method names, a bad request id, wrong parameters, a notification (no answer), a batch, an
|
|
answer to nothing, `rpc.discover` (its document must also equal `reports/json-rpc-api-schema.json` from
|
|
vanilla's data generator), `server/status`, the four lists, `players`, kicking nobody, `server/save` and
|
|
`server/system_message` with no one online, the four lists changed (`add`, `remove`, `set`,
|
|
`clear`; with no profile service, players named resolve to offline players on both), the live server
|
|
message, difficulty, default and forced game mode read and changed (including saved properties), operator
|
|
permission level read and changed (including saved properties and restart), autosave read and changed
|
|
(including its reset after restart), spawn-protection radius read and changed (including invalid input,
|
|
saved properties and restart), accepts-transfers and allow-flight read and changed (including invalid input,
|
|
saved properties and restart), hidden online players read and changed (including invalid input, saved properties and restart),
|
|
status replies read and changed (including invalid input, saved properties and restart), allowlist use and
|
|
enforcement read and changed
|
|
(including saved properties and restart), max players read and changed (including saved
|
|
properties and restart), player idle timeout read and changed (including disabling it, invalid input,
|
|
saved properties and restart), the live pause-when-empty timer read and changed (including its saved
|
|
property and restart), the live view distance read and changed (including invalid input, saved
|
|
properties and restart), the live simulation distance read and changed (including invalid input, saved
|
|
properties and restart), the live entity broadcast range read and changed (including invalid input,
|
|
saved properties and restart), and `server/stop`, after which each process ends as vanilla's does (how long
|
|
it took is recorded);
|
|
- the TLS version each server agrees on.
|
|
|
|
A parse error's `data` is Gson's message in vanilla and serde's in Horizon: left out. The notifications the
|
|
servers send meanwhile (`server/stopping`...) are set aside, and their names are kept in each side's report
|
|
(`notifications`); Horizon sends `server/started` and `server/stopping` so far, not yet the rest.
|
|
"""
|
|
import base64
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import socket
|
|
import ssl
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
try:
|
|
from . import lab, oracle_harness as oh, test_rust_parity as base
|
|
from .server_bench import DONE, Server
|
|
except ImportError:
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import lab
|
|
import oracle_harness as oh
|
|
import test_rust_parity as base
|
|
from server_bench import DONE, Server
|
|
|
|
NAME = 'management-protocol-parity'
|
|
EXE = '.exe' if os.name == 'nt' else ''
|
|
SECRET = 'abcdefghijABCDEFGHIJ0123456789abcdefghij'
|
|
ORIGIN = 'http://localhost:3000'
|
|
KIM = {'uuid': '93e029a5-04a0-349d-90b2-f7e145b2644f', 'name': 'Kim0'}
|
|
LOU = {'uuid': '94d84bd8-1dc1-3f9b-88b6-c0c9dc9aa57d', 'name': 'Lou0'}
|
|
LISTS = {
|
|
'ops.json': [dict(KIM, level=3, bypassesPlayerLimit=True)],
|
|
'whitelist.json': [LOU],
|
|
'banned-players.json': [dict(LOU, created='2026-10-31 14:00:00 +0000', source='Server',
|
|
expires='2026-11-30 12:30:00 +0000', reason='griefing')],
|
|
'banned-ips.json': [{'ip': '203.0.113.7', 'created': '2026-10-31 14:00:00 +0000', 'source': 'Server',
|
|
'expires': 'forever', 'reason': 'Banned by an operator.'}],
|
|
}
|
|
KEYSTORE_PASSWORD = 'harness-keystore-password'
|
|
# Nothing listens there: neither server finds a profile service, so names resolve to offline players on both,
|
|
# whatever the network answers.
|
|
NO_SERVICES = 'http://127.0.0.1:9'
|
|
# What both servers are sent over the socket, in order; None marks what must get no answer.
|
|
MESSAGES = [
|
|
'{nope',
|
|
'5',
|
|
'{"jsonrpc":"2.0","id":1,"method":"minecraft:nope"}',
|
|
'{"jsonrpc":"2.0","id":2,"method":"Bad Name"}',
|
|
'{"jsonrpc":"2.0","id":{"a":1},"method":"minecraft:players"}',
|
|
'{"jsonrpc":"2.0","id":3,"method":"rpc.discover","params":{}}',
|
|
'{"jsonrpc":"2.0","id":4,"method":"minecraft:players","params":[1]}',
|
|
'{"jsonrpc":"2.0","id":5,"method":"minecraft:server/save"}',
|
|
'{"jsonrpc":"2.0","id":6,"method":"minecraft:server/save","params":{"other":true}}',
|
|
'{"jsonrpc":"2.0","id":7,"method":"minecraft:server/save","params":[true,false]}',
|
|
'{"jsonrpc":"2.0","method":"minecraft:server/status"}',
|
|
'{"jsonrpc":"2.0","id":8,"result":true}',
|
|
'{"jsonrpc":"2.0","id":9}',
|
|
'[{"jsonrpc":"2.0","id":10,"method":"minecraft:allowlist"},{"jsonrpc":"2.0","method":"minecraft:bans"},'
|
|
'{"jsonrpc":"2.0","id":"eleven","method":"minecraft:operators","params":[]}]',
|
|
'[]',
|
|
'{"jsonrpc":"2.0","id":12,"method":"rpc.discover"}',
|
|
'{"jsonrpc":"2.0","id":13,"method":"server/status"}',
|
|
'{"jsonrpc":"2.0","id":null,"method":"minecraft:allowlist"}',
|
|
'{"jsonrpc":"2.0","id":14,"method":"minecraft:bans"}',
|
|
'{"jsonrpc":"2.0","id":15,"method":"minecraft:ip_bans"}',
|
|
'{"jsonrpc":"2.0","id":16,"method":"minecraft:operators"}',
|
|
'{"jsonrpc":"2.0","id":17,"method":"minecraft:players"}',
|
|
'{"jsonrpc":"2.0","id":18,"method":"minecraft:players/kick","params":{"kick":[{"player":{"name":"Nobody"}}]}}',
|
|
'{"jsonrpc":"2.0","id":19,"method":"minecraft:server/save","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":20,"method":"minecraft:server/system_message",'
|
|
'"params":[{"message":{"literal":"Hello"},"overlay":false}]}',
|
|
'{"jsonrpc":"2.0","id":21,"method":"minecraft:server/system_message",'
|
|
'"params":[{"message":{"translatable":"chat.type.announcement","translatableParams":["a","b"]},'
|
|
'"overlay":true,"receivingPlayers":[]}]}',
|
|
'{"jsonrpc":"2.0","id":22,"method":"minecraft:server/system_message","params":[{"message":{},"overlay":false}]}',
|
|
# The lists changed: players found by name (offline players here) or by an id the user cache knows.
|
|
'{"jsonrpc":"2.0","id":23,"method":"minecraft:allowlist/add","params":[[{"name":"Kim0"}]]}',
|
|
'{"jsonrpc":"2.0","id":24,"method":"minecraft:allowlist/remove","params":{"players":[{"name":"Lou0"}]}}',
|
|
'{"jsonrpc":"2.0","id":25,"method":"minecraft:allowlist/set","params":[[{"name":"Ann0"},'
|
|
'{"id":"93e029a5-04a0-349d-90b2-f7e145b2644f"}]]}',
|
|
'{"jsonrpc":"2.0","id":26,"method":"minecraft:allowlist/clear"}',
|
|
'{"jsonrpc":"2.0","id":27,"method":"minecraft:bans/add","params":[[{"player":{"name":"Kim0"},"reason":"spam",'
|
|
'"expires":"2027-01-01T00:00:00Z"},{"player":{"name":"Ann0"}}]]}',
|
|
'{"jsonrpc":"2.0","id":28,"method":"minecraft:bans/remove","params":[[{"name":"Lou0"}]]}',
|
|
'{"jsonrpc":"2.0","id":29,"method":"minecraft:bans/set","params":[[{"player":{"name":"Kim0"},"reason":"spam",'
|
|
'"expires":"2027-01-01T00:00:00Z"},{"player":{"name":"Ann0"},"source":"Moderator"}]]}',
|
|
'{"jsonrpc":"2.0","id":30,"method":"minecraft:bans"}',
|
|
'{"jsonrpc":"2.0","id":31,"method":"minecraft:bans/clear"}',
|
|
'{"jsonrpc":"2.0","id":32,"method":"minecraft:ip_bans/add","params":[[{"ip":"198.51.100.4","reason":"proxy",'
|
|
'"expires":"2027-01-01T00:00:00Z"},{"ip":"not an address"},{"player":{"name":"Nobody"}}]]}',
|
|
'{"jsonrpc":"2.0","id":33,"method":"minecraft:ip_bans/remove","params":[["203.0.113.7"]]}',
|
|
'{"jsonrpc":"2.0","id":34,"method":"minecraft:ip_bans/set","params":[[{"ip":"198.51.100.4","reason":"proxy",'
|
|
'"expires":"2027-01-01T00:00:00Z"},{"ip":"2001:db8::1","source":"Bot"},{"ip":"bad"}]]}',
|
|
'{"jsonrpc":"2.0","id":35,"method":"minecraft:ip_bans/clear"}',
|
|
'{"jsonrpc":"2.0","id":36,"method":"minecraft:operators/add","params":[[{"player":{"name":"Lou0"},'
|
|
'"permissionLevel":2},{"player":{"name":"Ann0"}}]]}',
|
|
'{"jsonrpc":"2.0","id":37,"method":"minecraft:operators/set","params":[[{"player":{"name":"Lou0"},'
|
|
'"permissionLevel":2,"bypassesPlayerLimit":false},{"player":{"name":"Ann0"},"bypassesPlayerLimit":true},'
|
|
'{"player":{"id":"93e029a5-04a0-349d-90b2-f7e145b2644f"}}]]}',
|
|
'{"jsonrpc":"2.0","id":38,"method":"minecraft:operators/remove","params":[[{"name":"Lou0"}]]}',
|
|
'{"jsonrpc":"2.0","id":39,"method":"minecraft:operators/clear"}',
|
|
'{"jsonrpc":"2.0","id":40,"method":"minecraft:serversettings/motd"}',
|
|
'{"jsonrpc":"2.0","id":41,"method":"minecraft:serversettings/motd/set","params":["A test server"]}',
|
|
'{"jsonrpc":"2.0","id":42,"method":"minecraft:serversettings/motd"}',
|
|
'{"jsonrpc":"2.0","id":43,"method":"minecraft:serversettings/motd/set","params":[""]}',
|
|
'{"jsonrpc":"2.0","id":44,"method":"minecraft:serversettings/motd"}',
|
|
'{"jsonrpc":"2.0","id":45,"method":"minecraft:serversettings/difficulty"}',
|
|
'{"jsonrpc":"2.0","id":46,"method":"minecraft:serversettings/difficulty/set","params":["hard"]}',
|
|
'{"jsonrpc":"2.0","id":47,"method":"minecraft:serversettings/difficulty"}',
|
|
'{"jsonrpc":"2.0","id":48,"method":"minecraft:serversettings/difficulty/set","params":["peaceful"]}',
|
|
'{"jsonrpc":"2.0","id":49,"method":"minecraft:serversettings/difficulty"}',
|
|
'{"jsonrpc":"2.0","id":50,"method":"minecraft:serversettings/game_mode"}',
|
|
'{"jsonrpc":"2.0","id":51,"method":"minecraft:serversettings/game_mode/set","params":["creative"]}',
|
|
'{"jsonrpc":"2.0","id":52,"method":"minecraft:serversettings/game_mode"}',
|
|
'{"jsonrpc":"2.0","id":53,"method":"minecraft:serversettings/game_mode/set","params":["spectator"]}',
|
|
'{"jsonrpc":"2.0","id":54,"method":"minecraft:serversettings/game_mode"}',
|
|
'{"jsonrpc":"2.0","id":180,"method":"minecraft:serversettings/entity_broadcast_range"}',
|
|
'{"jsonrpc":"2.0","id":181,"method":"minecraft:serversettings/entity_broadcast_range/set","params":[60]}',
|
|
'{"jsonrpc":"2.0","id":182,"method":"minecraft:serversettings/entity_broadcast_range"}',
|
|
'{"jsonrpc":"2.0","id":183,"method":"minecraft:serversettings/entity_broadcast_range/set","params":[1]}',
|
|
'{"jsonrpc":"2.0","id":184,"method":"minecraft:serversettings/entity_broadcast_range"}',
|
|
'{"jsonrpc":"2.0","id":185,"method":"minecraft:serversettings/entity_broadcast_range/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":186,"method":"minecraft:serversettings/entity_broadcast_range"}',
|
|
'{"jsonrpc":"2.0","id":187,"method":"minecraft:serversettings/entity_broadcast_range/set","params":[80]}',
|
|
'{"jsonrpc":"2.0","id":188,"method":"minecraft:serversettings/entity_broadcast_range"}',
|
|
'{"jsonrpc":"2.0","id":170,"method":"minecraft:serversettings/simulation_distance"}',
|
|
'{"jsonrpc":"2.0","id":171,"method":"minecraft:serversettings/simulation_distance/set","params":[6]}',
|
|
'{"jsonrpc":"2.0","id":172,"method":"minecraft:serversettings/simulation_distance"}',
|
|
'{"jsonrpc":"2.0","id":173,"method":"minecraft:serversettings/simulation_distance/set","params":[1]}',
|
|
'{"jsonrpc":"2.0","id":174,"method":"minecraft:serversettings/simulation_distance"}',
|
|
'{"jsonrpc":"2.0","id":175,"method":"minecraft:serversettings/simulation_distance/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":176,"method":"minecraft:serversettings/simulation_distance"}',
|
|
'{"jsonrpc":"2.0","id":177,"method":"minecraft:serversettings/simulation_distance/set","params":[8]}',
|
|
'{"jsonrpc":"2.0","id":178,"method":"minecraft:serversettings/simulation_distance"}',
|
|
'{"jsonrpc":"2.0","id":160,"method":"minecraft:serversettings/view_distance"}',
|
|
'{"jsonrpc":"2.0","id":161,"method":"minecraft:serversettings/view_distance/set","params":[6]}',
|
|
'{"jsonrpc":"2.0","id":162,"method":"minecraft:serversettings/view_distance"}',
|
|
'{"jsonrpc":"2.0","id":163,"method":"minecraft:serversettings/view_distance/set","params":[1]}',
|
|
'{"jsonrpc":"2.0","id":164,"method":"minecraft:serversettings/view_distance"}',
|
|
'{"jsonrpc":"2.0","id":165,"method":"minecraft:serversettings/view_distance/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":166,"method":"minecraft:serversettings/view_distance"}',
|
|
'{"jsonrpc":"2.0","id":167,"method":"minecraft:serversettings/view_distance/set","params":[8]}',
|
|
'{"jsonrpc":"2.0","id":168,"method":"minecraft:serversettings/view_distance"}',
|
|
'{"jsonrpc":"2.0","id":220,"method":"minecraft:serversettings/operator_user_permission_level"}',
|
|
'{"jsonrpc":"2.0","id":221,"method":"minecraft:serversettings/operator_user_permission_level/set","params":[2]}',
|
|
'{"jsonrpc":"2.0","id":222,"method":"minecraft:serversettings/operator_user_permission_level"}',
|
|
'{"jsonrpc":"2.0","id":223,"method":"minecraft:serversettings/operator_user_permission_level/set","params":[0]}',
|
|
'{"jsonrpc":"2.0","id":224,"method":"minecraft:serversettings/operator_user_permission_level"}',
|
|
'{"jsonrpc":"2.0","id":225,"method":"minecraft:serversettings/operator_user_permission_level/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":226,"method":"minecraft:serversettings/operator_user_permission_level"}',
|
|
'{"jsonrpc":"2.0","id":227,"method":"minecraft:serversettings/operator_user_permission_level/set","params":[2]}',
|
|
'{"jsonrpc":"2.0","id":228,"method":"minecraft:serversettings/operator_user_permission_level"}',
|
|
'{"jsonrpc":"2.0","id":55,"method":"minecraft:serversettings/force_game_mode"}',
|
|
'{"jsonrpc":"2.0","id":56,"method":"minecraft:serversettings/force_game_mode/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":57,"method":"minecraft:serversettings/force_game_mode"}',
|
|
'{"jsonrpc":"2.0","id":58,"method":"minecraft:serversettings/force_game_mode/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":59,"method":"minecraft:serversettings/force_game_mode"}',
|
|
'{"jsonrpc":"2.0","id":60,"method":"minecraft:serversettings/force_game_mode/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":61,"method":"minecraft:serversettings/force_game_mode"}',
|
|
'{"jsonrpc":"2.0","id":62,"method":"minecraft:serversettings/force_game_mode/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":63,"method":"minecraft:serversettings/force_game_mode"}',
|
|
'{"jsonrpc":"2.0","id":150,"method":"minecraft:serversettings/spawn_protection_radius"}',
|
|
'{"jsonrpc":"2.0","id":151,"method":"minecraft:serversettings/spawn_protection_radius/set","params":[4]}',
|
|
'{"jsonrpc":"2.0","id":152,"method":"minecraft:serversettings/spawn_protection_radius"}',
|
|
'{"jsonrpc":"2.0","id":153,"method":"minecraft:serversettings/spawn_protection_radius/set","params":[-1]}',
|
|
'{"jsonrpc":"2.0","id":154,"method":"minecraft:serversettings/spawn_protection_radius"}',
|
|
'{"jsonrpc":"2.0","id":155,"method":"minecraft:serversettings/spawn_protection_radius/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":156,"method":"minecraft:serversettings/spawn_protection_radius"}',
|
|
'{"jsonrpc":"2.0","id":157,"method":"minecraft:serversettings/spawn_protection_radius/set","params":[8]}',
|
|
'{"jsonrpc":"2.0","id":158,"method":"minecraft:serversettings/spawn_protection_radius"}',
|
|
'{"jsonrpc":"2.0","id":140,"method":"minecraft:serversettings/allow_flight"}',
|
|
'{"jsonrpc":"2.0","id":141,"method":"minecraft:serversettings/allow_flight/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":142,"method":"minecraft:serversettings/allow_flight"}',
|
|
'{"jsonrpc":"2.0","id":143,"method":"minecraft:serversettings/allow_flight/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":144,"method":"minecraft:serversettings/allow_flight"}',
|
|
'{"jsonrpc":"2.0","id":145,"method":"minecraft:serversettings/allow_flight/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":146,"method":"minecraft:serversettings/allow_flight"}',
|
|
'{"jsonrpc":"2.0","id":147,"method":"minecraft:serversettings/allow_flight/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":148,"method":"minecraft:serversettings/allow_flight"}',
|
|
'{"jsonrpc":"2.0","id":300,"method":"minecraft:serversettings/accept_transfers"}',
|
|
'{"jsonrpc":"2.0","id":301,"method":"minecraft:serversettings/accept_transfers/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":302,"method":"minecraft:serversettings/accept_transfers"}',
|
|
'{"jsonrpc":"2.0","id":303,"method":"minecraft:serversettings/accept_transfers/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":304,"method":"minecraft:serversettings/accept_transfers"}',
|
|
'{"jsonrpc":"2.0","id":305,"method":"minecraft:serversettings/accept_transfers/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":306,"method":"minecraft:serversettings/accept_transfers"}',
|
|
'{"jsonrpc":"2.0","id":307,"method":"minecraft:serversettings/accept_transfers/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":190,"method":"minecraft:serversettings/hide_online_players"}',
|
|
'{"jsonrpc":"2.0","id":191,"method":"minecraft:serversettings/hide_online_players/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":192,"method":"minecraft:serversettings/hide_online_players"}',
|
|
'{"jsonrpc":"2.0","id":193,"method":"minecraft:serversettings/hide_online_players/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":194,"method":"minecraft:serversettings/hide_online_players"}',
|
|
'{"jsonrpc":"2.0","id":195,"method":"minecraft:serversettings/hide_online_players/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":196,"method":"minecraft:serversettings/hide_online_players"}',
|
|
'{"jsonrpc":"2.0","id":197,"method":"minecraft:serversettings/hide_online_players/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":198,"method":"minecraft:serversettings/hide_online_players"}',
|
|
'{"jsonrpc":"2.0","id":210,"method":"minecraft:serversettings/status_replies"}',
|
|
'{"jsonrpc":"2.0","id":211,"method":"minecraft:serversettings/status_replies/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":212,"method":"minecraft:serversettings/status_replies"}',
|
|
'{"jsonrpc":"2.0","id":213,"method":"minecraft:serversettings/status_replies/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":214,"method":"minecraft:serversettings/status_replies"}',
|
|
'{"jsonrpc":"2.0","id":215,"method":"minecraft:serversettings/status_replies/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":216,"method":"minecraft:serversettings/status_replies"}',
|
|
'{"jsonrpc":"2.0","id":217,"method":"minecraft:serversettings/status_replies/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":218,"method":"minecraft:serversettings/status_replies"}',
|
|
'{"jsonrpc":"2.0","id":65,"method":"minecraft:serversettings/autosave"}',
|
|
'{"jsonrpc":"2.0","id":66,"method":"minecraft:serversettings/autosave/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":67,"method":"minecraft:serversettings/autosave"}',
|
|
'{"jsonrpc":"2.0","id":68,"method":"minecraft:serversettings/autosave/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":69,"method":"minecraft:serversettings/autosave"}',
|
|
'{"jsonrpc":"2.0","id":70,"method":"minecraft:serversettings/autosave/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":71,"method":"minecraft:serversettings/autosave"}',
|
|
'{"jsonrpc":"2.0","id":72,"method":"minecraft:serversettings/autosave/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":73,"method":"minecraft:serversettings/autosave"}',
|
|
'{"jsonrpc":"2.0","id":75,"method":"minecraft:serversettings/use_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":76,"method":"minecraft:serversettings/use_allowlist/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":77,"method":"minecraft:serversettings/use_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":78,"method":"minecraft:serversettings/use_allowlist/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":79,"method":"minecraft:serversettings/use_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":80,"method":"minecraft:serversettings/use_allowlist/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":81,"method":"minecraft:serversettings/use_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":82,"method":"minecraft:serversettings/use_allowlist/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":83,"method":"minecraft:serversettings/use_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":84,"method":"minecraft:serversettings/enforce_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":85,"method":"minecraft:serversettings/enforce_allowlist/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":86,"method":"minecraft:serversettings/enforce_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":87,"method":"minecraft:serversettings/enforce_allowlist/set","params":[false]}',
|
|
'{"jsonrpc":"2.0","id":88,"method":"minecraft:serversettings/enforce_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":89,"method":"minecraft:serversettings/enforce_allowlist/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":90,"method":"minecraft:serversettings/enforce_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":91,"method":"minecraft:serversettings/enforce_allowlist/set","params":[true]}',
|
|
'{"jsonrpc":"2.0","id":92,"method":"minecraft:serversettings/enforce_allowlist"}',
|
|
'{"jsonrpc":"2.0","id":100,"method":"minecraft:serversettings/max_players"}',
|
|
'{"jsonrpc":"2.0","id":101,"method":"minecraft:serversettings/max_players/set","params":[10]}',
|
|
'{"jsonrpc":"2.0","id":102,"method":"minecraft:serversettings/max_players"}',
|
|
'{"jsonrpc":"2.0","id":103,"method":"minecraft:serversettings/max_players/set","params":[30]}',
|
|
'{"jsonrpc":"2.0","id":104,"method":"minecraft:serversettings/max_players"}',
|
|
'{"jsonrpc":"2.0","id":105,"method":"minecraft:serversettings/max_players/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":106,"method":"minecraft:serversettings/max_players"}',
|
|
'{"jsonrpc":"2.0","id":107,"method":"minecraft:serversettings/max_players/set","params":[30]}',
|
|
'{"jsonrpc":"2.0","id":108,"method":"minecraft:serversettings/max_players"}',
|
|
# The game rules: the list, a rule set (and set again to the same value), an integer rule, and what doesn't fit.
|
|
'{"jsonrpc":"2.0","id":200,"method":"minecraft:gamerules"}',
|
|
'{"jsonrpc":"2.0","id":201,"method":"minecraft:gamerules/update","params":[{"key":"minecraft:pvp","value":false}]}',
|
|
'{"jsonrpc":"2.0","id":202,"method":"minecraft:gamerules/update","params":[{"key":"minecraft:pvp","value":false}]}',
|
|
'{"jsonrpc":"2.0","id":203,"method":"minecraft:gamerules/update",'
|
|
'"params":[{"key":"minecraft:random_tick_speed","value":5}]}',
|
|
'{"jsonrpc":"2.0","id":204,"method":"minecraft:gamerules/update",'
|
|
'"params":[{"key":"minecraft:random_tick_speed","value":-1}]}',
|
|
'{"jsonrpc":"2.0","id":205,"method":"minecraft:gamerules/update","params":[{"key":"minecraft:pvp","value":"yes"}]}',
|
|
'{"jsonrpc":"2.0","id":206,"method":"minecraft:gamerules/update","params":[{"key":"minecraft:nope","value":true}]}',
|
|
'{"jsonrpc":"2.0","id":207,"method":"minecraft:gamerules/update","params":[{"value":true}]}',
|
|
'{"jsonrpc":"2.0","id":208,"method":"minecraft:gamerules"}',
|
|
'{"jsonrpc":"2.0","id":400,"method":"minecraft:serversettings/status_heartbeat_interval"}',
|
|
'{"jsonrpc":"2.0","id":401,"method":"minecraft:serversettings/status_heartbeat_interval/set","params":[30]}',
|
|
'{"jsonrpc":"2.0","id":402,"method":"minecraft:serversettings/status_heartbeat_interval"}',
|
|
'{"jsonrpc":"2.0","id":403,"method":"minecraft:serversettings/status_heartbeat_interval/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":404,"method":"minecraft:serversettings/status_heartbeat_interval"}',
|
|
# -5 cancels the running 30 s heartbeat and schedules none; ManagementServer keeps the cancelled future,
|
|
# so the set to 30 that follows is refused (cancel(true) returns false) and -5 is what is saved.
|
|
'{"jsonrpc":"2.0","id":405,"method":"minecraft:serversettings/status_heartbeat_interval/set","params":[-5]}',
|
|
'{"jsonrpc":"2.0","id":406,"method":"minecraft:serversettings/status_heartbeat_interval"}',
|
|
'{"jsonrpc":"2.0","id":407,"method":"minecraft:serversettings/status_heartbeat_interval/set","params":[30]}',
|
|
'{"jsonrpc":"2.0","id":120,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":121,"method":"minecraft:serversettings/player_idle_timeout/set","params":[10]}',
|
|
'{"jsonrpc":"2.0","id":122,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":123,"method":"minecraft:serversettings/player_idle_timeout/set","params":[0]}',
|
|
'{"jsonrpc":"2.0","id":124,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":125,"method":"minecraft:serversettings/player_idle_timeout/set","params":[-1]}',
|
|
'{"jsonrpc":"2.0","id":126,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":127,"method":"minecraft:serversettings/player_idle_timeout/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":128,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":134,"method":"minecraft:serversettings/player_idle_timeout/set","params":[1.9]}',
|
|
'{"jsonrpc":"2.0","id":135,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":136,"method":"minecraft:serversettings/player_idle_timeout/set","params":[4294967297]}',
|
|
'{"jsonrpc":"2.0","id":137,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":138,"method":"minecraft:serversettings/player_idle_timeout/set","params":[-4294967297]}',
|
|
'{"jsonrpc":"2.0","id":139,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":129,"method":"minecraft:serversettings/player_idle_timeout/set","params":[30]}',
|
|
'{"jsonrpc":"2.0","id":130,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":131,"method":"minecraft:serversettings/player_idle_timeout/set","params":[30]}',
|
|
'{"jsonrpc":"2.0","id":132,"method":"minecraft:serversettings/player_idle_timeout"}',
|
|
'{"jsonrpc":"2.0","id":110,"method":"minecraft:serversettings/pause_when_empty_seconds"}',
|
|
'{"jsonrpc":"2.0","id":111,"method":"minecraft:serversettings/pause_when_empty_seconds/set","params":[10]}',
|
|
'{"jsonrpc":"2.0","id":112,"method":"minecraft:serversettings/pause_when_empty_seconds"}',
|
|
'{"jsonrpc":"2.0","id":113,"method":"minecraft:serversettings/pause_when_empty_seconds/set","params":[30]}',
|
|
'{"jsonrpc":"2.0","id":114,"method":"minecraft:serversettings/pause_when_empty_seconds"}',
|
|
'{"jsonrpc":"2.0","id":115,"method":"minecraft:serversettings/pause_when_empty_seconds/set","params":["yes"]}',
|
|
'{"jsonrpc":"2.0","id":116,"method":"minecraft:serversettings/pause_when_empty_seconds"}',
|
|
'{"jsonrpc":"2.0","id":117,"method":"minecraft:serversettings/pause_when_empty_seconds/set","params":[30]}',
|
|
'{"jsonrpc":"2.0","id":118,"method":"minecraft:serversettings/pause_when_empty_seconds"}',
|
|
]
|
|
# The notifications Horizon sends so far, which both servers must send for the same messages the same number of
|
|
# times (the others, `server/saving`... depend on timing, or aren't sent yet).
|
|
COMPARED_NOTIFICATIONS = {
|
|
f'minecraft:notification/{name}' for name in (
|
|
'gamerules/updated', 'operators/added', 'operators/removed', 'allowlist/added', 'allowlist/removed',
|
|
'bans/added', 'bans/removed', 'ip_bans/added', 'ip_bans/removed')}
|
|
|
|
|
|
def management_properties(port, keystore):
|
|
return {
|
|
'management-server-enabled': 'true',
|
|
'management-server-host': '127.0.0.1',
|
|
'management-server-port': str(port),
|
|
'management-server-secret': SECRET,
|
|
'management-server-tls-enabled': 'true',
|
|
'management-server-tls-keystore': str(keystore),
|
|
'management-server-tls-keystore-password': KEYSTORE_PASSWORD,
|
|
'management-server-allowed-origins': ORIGIN,
|
|
}
|
|
|
|
|
|
def properties_value(value):
|
|
"""A `server.properties` value as vanilla writes it: Java's parser reads a backslash as the start of an
|
|
escape, so a Windows path (the keystore) must have its own doubled."""
|
|
return value.replace('\\', '\\\\')
|
|
|
|
|
|
def make_keystore(work):
|
|
"""A PKCS12 keystore with a self-signed certificate for 127.0.0.1, made with the JDK's keytool, and a TLS
|
|
client context that trusts it."""
|
|
keytool = shutil.which('keytool')
|
|
if keytool is None:
|
|
java_home = os.environ.get('JAVA_HOME')
|
|
candidate = Path(java_home) / 'bin' / f'keytool{EXE}' if java_home else None
|
|
if candidate is None or not candidate.is_file():
|
|
raise RuntimeError('keytool not found; add a JDK to PATH or set JAVA_HOME')
|
|
keytool = str(candidate)
|
|
keystore, certificate = work / 'management.p12', work / 'management.pem'
|
|
common = ['-storetype', 'PKCS12', '-keystore', str(keystore), '-storepass', KEYSTORE_PASSWORD]
|
|
subprocess.run([keytool, '-genkeypair', '-alias', 'management', '-keyalg', 'RSA', '-keysize', '2048',
|
|
'-dname', 'CN=localhost', '-ext', 'SAN=dns:localhost,ip:127.0.0.1', '-validity', '2',
|
|
'-keypass', KEYSTORE_PASSWORD, *common], check=True, capture_output=True)
|
|
subprocess.run([keytool, '-exportcert', '-rfc', '-alias', 'management', '-file', str(certificate), *common],
|
|
check=True, capture_output=True)
|
|
context = ssl.create_default_context(cafile=str(certificate))
|
|
# A refused request's connection may end without TLS's close_notify.
|
|
context.options |= getattr(ssl, 'OP_IGNORE_UNEXPECTED_EOF', 0)
|
|
return keystore, context
|
|
|
|
|
|
def connect(port, context):
|
|
sock = socket.create_connection(('127.0.0.1', port), timeout=60)
|
|
return context.wrap_socket(sock, server_hostname='127.0.0.1')
|
|
|
|
|
|
def write_lists(folder):
|
|
for file, entries in LISTS.items():
|
|
(folder / file).write_text(json.dumps(entries, indent=2))
|
|
(folder / 'eula.txt').write_text('eula=true\n')
|
|
|
|
|
|
class Socket:
|
|
"""A WebSocket client: the handshake, masked text frames out, text frames in."""
|
|
|
|
def __init__(self, port, context, headers):
|
|
self.sock = connect(port, context)
|
|
key = base64.b64encode(os.urandom(16)).decode()
|
|
lines = [f'GET / HTTP/1.1', f'Host: 127.0.0.1:{port}', 'Upgrade: websocket', 'Connection: Upgrade',
|
|
f'Sec-WebSocket-Key: {key}', 'Sec-WebSocket-Version: 13', *headers]
|
|
self.sock.sendall(('\r\n'.join(lines) + '\r\n\r\n').encode())
|
|
self.buffer = b''
|
|
while b'\r\n\r\n' not in self.buffer:
|
|
data = self.sock.recv(4096)
|
|
if not data:
|
|
break
|
|
self.buffer += data
|
|
head, _, self.buffer = self.buffer.partition(b'\r\n\r\n')
|
|
self.head = head.decode('iso-8859-1').split('\r\n')
|
|
|
|
def header(self, name):
|
|
for line in self.head[1:]:
|
|
k, _, v = line.partition(':')
|
|
if k.strip().lower() == name.lower():
|
|
return v.strip()
|
|
return None
|
|
|
|
def send(self, text):
|
|
payload = text.encode()
|
|
mask = os.urandom(4)
|
|
length = len(payload)
|
|
head = bytes([0x81]) + (bytes([0x80 | length]) if length < 126 else
|
|
bytes([0x80 | 126]) + struct.pack('>H', length) if length < 65536 else
|
|
bytes([0x80 | 127]) + struct.pack('>Q', length))
|
|
self.sock.sendall(head + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))
|
|
|
|
def _read(self, n):
|
|
while len(self.buffer) < n:
|
|
data = self.sock.recv(65536)
|
|
if not data:
|
|
raise EOFError('the server closed the socket')
|
|
self.buffer += data
|
|
out, self.buffer = self.buffer[:n], self.buffer[n:]
|
|
return out
|
|
|
|
def receive(self, timeout):
|
|
"""The next text message, or None if none comes within the timeout."""
|
|
self.sock.settimeout(timeout)
|
|
try:
|
|
first = self._read(2)
|
|
except socket.timeout:
|
|
return None
|
|
self.sock.settimeout(60)
|
|
length = first[1] & 0x7f
|
|
if length == 126:
|
|
length = struct.unpack('>H', self._read(2))[0]
|
|
elif length == 127:
|
|
length = struct.unpack('>Q', self._read(8))[0]
|
|
payload = self._read(length)
|
|
if first[0] & 0x0f != 1:
|
|
raise AssertionError(f'expected a text frame, got opcode {first[0] & 0x0f}')
|
|
return payload.decode()
|
|
|
|
|
|
def http_status(port, context, headers):
|
|
"""The status line and body of a request the server must refuse."""
|
|
with connect(port, context) as sock:
|
|
lines = ['GET / HTTP/1.1', f'Host: 127.0.0.1:{port}', 'Upgrade: websocket', 'Connection: Upgrade',
|
|
'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==', 'Sec-WebSocket-Version: 13', *headers]
|
|
sock.sendall(('\r\n'.join(lines) + '\r\n\r\n').encode())
|
|
data = b''
|
|
while True:
|
|
chunk = sock.recv(4096)
|
|
if not chunk:
|
|
break
|
|
data += chunk
|
|
head, _, body = data.partition(b'\r\n\r\n')
|
|
if head and b'Content-Length' in head:
|
|
length = int(next(l.split(b':')[1] for l in head.split(b'\r\n')
|
|
if l.lower().startswith(b'content-length')))
|
|
if len(body) >= length:
|
|
break
|
|
head, _, body = data.partition(b'\r\n\r\n')
|
|
return head.split(b'\r\n')[0].split(b' ')[1].decode(), body.decode()
|
|
|
|
|
|
def normal(text):
|
|
"""An answer as compared: parsed, with a parse error's message left out."""
|
|
if text is None:
|
|
return None
|
|
value = json.loads(text)
|
|
if isinstance(value, dict) and value.get('error', {}).get('code') == -32700:
|
|
value['error'].pop('data', None)
|
|
# The game rules come in the order of a map keyed by identity in vanilla (it changes from run to run).
|
|
result = value.get('result') if isinstance(value, dict) else None
|
|
if isinstance(result, list) and result and all(isinstance(r, dict) and 'key' in r for r in result):
|
|
value['result'] = sorted(result, key=lambda rule: rule['key'])
|
|
return value
|
|
|
|
|
|
def session(port, context, log):
|
|
"""Everything one server answers."""
|
|
seen = {}
|
|
for label, headers in [('no secret', []), ('wrong secret', ['Authorization: Bearer nope']),
|
|
('origin not allowed', [f'Sec-WebSocket-Protocol: minecraft-v1,{SECRET}',
|
|
'Origin: http://evil.example'])]:
|
|
seen[label] = http_status(port, context, headers)
|
|
browser = Socket(port, context, [f'Sec-WebSocket-Protocol: minecraft-v1, {SECRET}', f'Origin: {ORIGIN}'])
|
|
seen['subprotocol'] = (browser.head[0].split(' ')[1], browser.header('Sec-WebSocket-Protocol'))
|
|
browser.sock.close()
|
|
ws = Socket(port, context, [f'Authorization: Bearer {SECRET}'])
|
|
if ws.head[0].split(' ')[1] != '101':
|
|
raise AssertionError(f'the handshake failed: {ws.head}')
|
|
seen['tls'] = ws.sock.version()
|
|
notifications = []
|
|
|
|
def answer(message):
|
|
"""The answer to a message, the notifications that come first set aside."""
|
|
ws.send(message)
|
|
while True:
|
|
text = ws.receive(timeout=3 if '"id"' not in message or '"result"' in message else 60)
|
|
log.write(f'> {message}\n< {text}\n')
|
|
value = normal(text)
|
|
if isinstance(value, dict) and 'method' in value and 'id' not in value:
|
|
notifications.append(value['method'])
|
|
continue
|
|
return value
|
|
|
|
seen['answers'] = [answer(message) for message in MESSAGES]
|
|
seen['stop'] = answer('{"jsonrpc":"2.0","id":99,"method":"minecraft:server/stop"}')
|
|
seen['notifications'] = notifications
|
|
return seen
|
|
|
|
|
|
def wait_for_exit(server, side, timeout):
|
|
"""The seconds the server's process took to end after `server/stop`, how long it was given.
|
|
|
|
Vanilla's exits: `net.minecraft.server.jsonrpc.methods.ServerStateService.stop` submits
|
|
`MinecraftServer.halt(false)`, `runServer` returns and `main` ends the process. A server still running is
|
|
the regression this harness exists to catch, so the failure names the side and the last lines it logged,
|
|
not only Python's `TimeoutExpired`."""
|
|
asked = time.time()
|
|
while time.time() - asked < timeout:
|
|
if server.process.poll() is not None:
|
|
return round(time.time() - asked, 1)
|
|
time.sleep(0.1)
|
|
raise AssertionError(
|
|
f'the {side} server was still running {timeout} s after server/stop, where vanilla\'s process '
|
|
f'exits:\n' + '\n'.join(server.since(0)[-10:]))
|
|
|
|
|
|
def main():
|
|
lock, jars, libraries = base.verify_artifacts()
|
|
build = lab.LOCAL / NAME
|
|
shutil.rmtree(build, ignore_errors=True)
|
|
build.mkdir(parents=True)
|
|
oh.run_logged(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client',
|
|
'-p', 'horizon-server', '--bin', 'horizon-server'], build, 'compile-rust')
|
|
target = lab.cargo_target() / 'fast'
|
|
work = Path(tempfile.mkdtemp(prefix='hz-management-'))
|
|
os.environ['HORIZON_API_DISCOVERY'] = NO_SERVICES
|
|
try:
|
|
keystore, context = make_keystore(work)
|
|
sides = {}
|
|
for side in ('vanilla', 'horizon'):
|
|
folder = work / side
|
|
folder.mkdir()
|
|
write_lists(folder)
|
|
port, management = lab.free_port(), lab.free_port()
|
|
props = {'server-port': str(port), 'online-mode': 'false', 'level-type': 'minecraft:flat',
|
|
'generate-structures': 'false', 'view-distance': '3', 'simulation-distance': '3',
|
|
**management_properties(management, keystore)}
|
|
if side == 'vanilla':
|
|
(folder / 'server.properties').write_text(''.join(f'{k}={properties_value(v)}\n'
|
|
for k, v in props.items()))
|
|
command = ['java', '-Xmx2g', '-Duser.timezone=UTC', f'-Dminecraft.api.discovery.host={NO_SERVICES}',
|
|
'-cp',
|
|
os.pathsep.join(map(str, [jars['server'], *libraries])), 'net.minecraft.server.Main',
|
|
'nogui']
|
|
else:
|
|
typed = ('server-port', 'online-mode', 'view-distance', 'simulation-distance')
|
|
toml = ''.join(f'"{k}" = {json.dumps(v)}\n' for k, v in props.items() if k not in typed)
|
|
(folder / 'server.toml').write_text(
|
|
f'[server]\nport = {port}\nonline_mode = false\nview_distance = 3\nsimulation_distance = 3\n\n'
|
|
f'[properties]\n{toml}\n'
|
|
f'[engine]\npath = {json.dumps(str(target / f"horizon-client{EXE}"))}\n'
|
|
f'resources = {json.dumps(str(lab.RESOURCES.resolve()))}\nrestart_on_crash = false\n\n'
|
|
'[backups]\nevery = "0"\n\n[panel]\nbind = ""\n')
|
|
command = [target / f'horizon-server{EXE}', '--dir', folder, 'run']
|
|
server = Server(command, folder, build / f'{side}.log')
|
|
try:
|
|
server.wait_for(DONE, 600)
|
|
with (build / f'{side}-session.log').open('w') as log:
|
|
sides[side] = session(management, context, log)
|
|
sides[side]['stop_seconds'] = wait_for_exit(server, side, 180)
|
|
properties = (folder / 'server.properties').read_text()
|
|
sides[side]['saved_difficulty'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('difficulty=')), None)
|
|
sides[side]['saved_game_mode'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('gamemode=')), None)
|
|
sides[side]['saved_force_game_mode'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('force-gamemode=')), None)
|
|
sides[side]['saved_use_allowlist'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('white-list=')), None)
|
|
sides[side]['saved_enforce_allowlist'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('enforce-whitelist=')), None)
|
|
sides[side]['saved_pause_when_empty_seconds'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('pause-when-empty-seconds=')), None)
|
|
sides[side]['saved_max_players'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('max-players=')), None)
|
|
sides[side]['saved_status_heartbeat_interval'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('status-heartbeat-interval=')), None)
|
|
sides[side]['saved_player_idle_timeout'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('player-idle-timeout=')), None)
|
|
sides[side]['saved_entity_broadcast_range'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('entity-broadcast-range-percentage=')), None)
|
|
sides[side]['saved_simulation_distance'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('simulation-distance=')), None)
|
|
sides[side]['saved_view_distance'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('view-distance=')), None)
|
|
sides[side]['saved_spawn_protection'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('spawn-protection=')), None)
|
|
sides[side]['saved_operator_user_permission_level'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('op-permission-level=')), None)
|
|
sides[side]['saved_allow_flight'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('allow-flight=')), None)
|
|
sides[side]['saved_accepts_transfers'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('accepts-transfers=')), None)
|
|
sides[side]['saved_hide_online_players'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('hide-online-players=')), None)
|
|
sides[side]['saved_status_replies'] = next(
|
|
(line.partition('=')[2] for line in properties.splitlines()
|
|
if line.startswith('enable-status=')), None)
|
|
if side == 'horizon':
|
|
world = (folder / 'server.toml').read_text().partition('[world]')[2].split('\n[', 1)[0]
|
|
sides[side]['saved_config_difficulty'] = 'difficulty = "peaceful"' in world
|
|
sides[side]['saved_config_game_mode'] = 'game_mode = "spectator"' in world
|
|
sides[side]['saved_config_force_game_mode'] = 'force-gamemode = true' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_use_allowlist'] = 'whitelist = true' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_enforce_allowlist'] = 'enforce_whitelist = true' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_pause_when_empty_seconds'] = 'pause-when-empty-seconds = 30' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_max_players'] = 'max_players = 30' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_player_idle_timeout'] = 'player-idle-timeout = 30' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_entity_broadcast_range'] = 'entity-broadcast-range-percentage = 80' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_simulation_distance'] = 'simulation_distance = 8' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_view_distance'] = 'view_distance = 8' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_spawn_protection'] = 'spawn-protection = 8' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_operator_user_permission_level'] = 'op-permission-level = 2' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_allow_flight'] = 'allow-flight = true' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_accepts_transfers'] = 'accepts-transfers = true' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_hide_online_players'] = 'hide-online-players = true' in (
|
|
folder / 'server.toml').read_text()
|
|
sides[side]['saved_config_status_replies'] = 'enable-status = false' in (
|
|
folder / 'server.toml').read_text()
|
|
finally:
|
|
server.stop()
|
|
restarted = Server(command, folder, build / f'{side}-restart.log')
|
|
try:
|
|
restarted.wait_for(DONE, 600)
|
|
ws = Socket(management, context, [f'Authorization: Bearer {SECRET}'])
|
|
try:
|
|
ws.send('{"jsonrpc":"2.0","id":64,"method":"minecraft:serversettings/force_game_mode"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_force_game_mode'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":74,"method":"minecraft:serversettings/autosave"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_autosave'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":119,"method":"minecraft:serversettings/pause_when_empty_seconds"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_pause_when_empty_seconds'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":93,"method":"minecraft:serversettings/use_allowlist"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_use_allowlist'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":94,"method":"minecraft:serversettings/enforce_allowlist"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_enforce_allowlist'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":109,"method":"minecraft:serversettings/max_players"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_max_players'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":408,"method":"minecraft:serversettings/status_heartbeat_interval"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_status_heartbeat_interval'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":133,"method":"minecraft:serversettings/player_idle_timeout"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_player_idle_timeout'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":149,"method":"minecraft:serversettings/allow_flight"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_allow_flight'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":308,"method":"minecraft:serversettings/accept_transfers"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_accepts_transfers'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":189,"method":"minecraft:serversettings/entity_broadcast_range"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_entity_broadcast_range'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":179,"method":"minecraft:serversettings/simulation_distance"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_simulation_distance'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":169,"method":"minecraft:serversettings/view_distance"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_view_distance'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":159,"method":"minecraft:serversettings/spawn_protection_radius"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_spawn_protection'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":199,"method":"minecraft:serversettings/hide_online_players"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_hide_online_players'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":229,"method":"minecraft:serversettings/operator_user_permission_level"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_operator_user_permission_level'] = reply
|
|
ws.send('{"jsonrpc":"2.0","id":219,"method":"minecraft:serversettings/status_replies"}')
|
|
while True:
|
|
reply = normal(ws.receive(timeout=60))
|
|
if not isinstance(reply, dict) or 'method' not in reply or 'id' in reply:
|
|
break
|
|
sides[side]['restarted_status_replies'] = reply
|
|
finally:
|
|
ws.sock.close()
|
|
finally:
|
|
restarted.stop()
|
|
vanilla, horizon = sides['vanilla'], sides['horizon']
|
|
failures = []
|
|
for key in ('no secret', 'wrong secret', 'origin not allowed', 'subprotocol', 'tls', 'stop'):
|
|
if vanilla[key] != horizon[key]:
|
|
failures.append(f'{key}: vanilla {vanilla[key]!r}, Horizon {horizon[key]!r}')
|
|
for message, v, h in zip(MESSAGES, vanilla['answers'], horizon['answers']):
|
|
if v != h:
|
|
failures.append(f'{message}\n vanilla {json.dumps(v)[:400]}\n Horizon {json.dumps(h)[:400]}')
|
|
told = {side: sorted(n for n in sides[side]['notifications'] if n in COMPARED_NOTIFICATIONS)
|
|
for side in ('vanilla', 'horizon')}
|
|
if told['vanilla'] != told['horizon']:
|
|
failures.append(f"notifications: vanilla {told['vanilla']}\n Horizon {told['horizon']}")
|
|
if vanilla['saved_difficulty'] != horizon['saved_difficulty'] or vanilla['saved_difficulty'] != 'peaceful':
|
|
failures.append(f"saved difficulty: vanilla {vanilla['saved_difficulty']!r}, Horizon {horizon['saved_difficulty']!r}")
|
|
if not horizon['saved_config_difficulty']:
|
|
failures.append('Horizon did not save peaceful difficulty in server.toml')
|
|
if vanilla['saved_game_mode'] != horizon['saved_game_mode'] or vanilla['saved_game_mode'] != 'spectator':
|
|
failures.append(f"saved game mode: vanilla {vanilla['saved_game_mode']!r}, Horizon {horizon['saved_game_mode']!r}")
|
|
if not horizon['saved_config_game_mode']:
|
|
failures.append('Horizon did not save spectator game mode in server.toml')
|
|
if vanilla['saved_force_game_mode'] != horizon['saved_force_game_mode'] or vanilla['saved_force_game_mode'] != 'true':
|
|
failures.append(f"saved forced game mode: vanilla {vanilla['saved_force_game_mode']!r}, Horizon {horizon['saved_force_game_mode']!r}")
|
|
if not horizon['saved_config_force_game_mode']:
|
|
failures.append('Horizon did not save forced game mode in server.toml')
|
|
if vanilla['saved_use_allowlist'] != horizon['saved_use_allowlist'] or vanilla['saved_use_allowlist'] != 'true':
|
|
failures.append(f"saved allowlist use: vanilla {vanilla['saved_use_allowlist']!r}, Horizon {horizon['saved_use_allowlist']!r}")
|
|
if vanilla['saved_enforce_allowlist'] != horizon['saved_enforce_allowlist'] or vanilla['saved_enforce_allowlist'] != 'true':
|
|
failures.append(f"saved allowlist enforcement: vanilla {vanilla['saved_enforce_allowlist']!r}, Horizon {horizon['saved_enforce_allowlist']!r}")
|
|
if not horizon['saved_config_use_allowlist']:
|
|
failures.append('Horizon did not save allowlist use in server.toml')
|
|
if not horizon['saved_config_enforce_allowlist']:
|
|
failures.append('Horizon did not save allowlist enforcement in server.toml')
|
|
if vanilla['saved_pause_when_empty_seconds'] != horizon['saved_pause_when_empty_seconds'] or \
|
|
vanilla['saved_pause_when_empty_seconds'] != '30':
|
|
failures.append(f"saved pause-when-empty seconds: vanilla {vanilla['saved_pause_when_empty_seconds']!r}, Horizon {horizon['saved_pause_when_empty_seconds']!r}")
|
|
if not horizon['saved_config_pause_when_empty_seconds']:
|
|
failures.append('Horizon did not save pause-when-empty seconds in server.toml')
|
|
if vanilla['restarted_force_game_mode'] != horizon['restarted_force_game_mode'] or \
|
|
vanilla['restarted_force_game_mode'] != {'jsonrpc': '2.0', 'id': 64, 'result': True}:
|
|
failures.append(f"restarted forced game mode: vanilla {vanilla['restarted_force_game_mode']!r}, Horizon {horizon['restarted_force_game_mode']!r}")
|
|
if vanilla['restarted_autosave'] != horizon['restarted_autosave'] or \
|
|
vanilla['restarted_autosave'] != {'jsonrpc': '2.0', 'id': 74, 'result': True}:
|
|
failures.append(f"restarted autosave: vanilla {vanilla['restarted_autosave']!r}, Horizon {horizon['restarted_autosave']!r}")
|
|
if vanilla['restarted_pause_when_empty_seconds'] != horizon['restarted_pause_when_empty_seconds'] or \
|
|
vanilla['restarted_pause_when_empty_seconds'] != {'jsonrpc': '2.0', 'id': 119, 'result': 30}:
|
|
failures.append(f"restarted pause-when-empty seconds: vanilla {vanilla['restarted_pause_when_empty_seconds']!r}, Horizon {horizon['restarted_pause_when_empty_seconds']!r}")
|
|
if vanilla['restarted_use_allowlist'] != horizon['restarted_use_allowlist'] or \
|
|
vanilla['restarted_use_allowlist'] != {'jsonrpc': '2.0', 'id': 93, 'result': True}:
|
|
failures.append(f"restarted allowlist use: vanilla {vanilla['restarted_use_allowlist']!r}, Horizon {horizon['restarted_use_allowlist']!r}")
|
|
if vanilla['restarted_enforce_allowlist'] != horizon['restarted_enforce_allowlist'] or \
|
|
vanilla['restarted_enforce_allowlist'] != {'jsonrpc': '2.0', 'id': 94, 'result': True}:
|
|
failures.append(f"restarted allowlist enforcement: vanilla {vanilla['restarted_enforce_allowlist']!r}, Horizon {horizon['restarted_enforce_allowlist']!r}")
|
|
if vanilla['saved_max_players'] != horizon['saved_max_players'] or vanilla['saved_max_players'] != '30':
|
|
failures.append(f"saved max players: vanilla {vanilla['saved_max_players']!r}, Horizon {horizon['saved_max_players']!r}")
|
|
if not horizon['saved_config_max_players']:
|
|
failures.append('Horizon did not save max players in server.toml')
|
|
if vanilla['restarted_max_players'] != horizon['restarted_max_players'] or \
|
|
vanilla['restarted_max_players'] != {'jsonrpc': '2.0', 'id': 109, 'result': 30}:
|
|
failures.append(f"restarted max players: vanilla {vanilla['restarted_max_players']!r}, Horizon {horizon['restarted_max_players']!r}")
|
|
if vanilla['saved_status_heartbeat_interval'] != horizon['saved_status_heartbeat_interval'] or \
|
|
vanilla['saved_status_heartbeat_interval'] != '-5':
|
|
failures.append(f"saved status heartbeat interval: vanilla {vanilla['saved_status_heartbeat_interval']!r}, Horizon {horizon['saved_status_heartbeat_interval']!r}")
|
|
if vanilla['restarted_status_heartbeat_interval'] != horizon['restarted_status_heartbeat_interval'] or \
|
|
vanilla['restarted_status_heartbeat_interval'] != {'jsonrpc': '2.0', 'id': 408, 'result': -5}:
|
|
failures.append(f"restarted status heartbeat interval: vanilla {vanilla['restarted_status_heartbeat_interval']!r}, Horizon {horizon['restarted_status_heartbeat_interval']!r}")
|
|
if vanilla['saved_player_idle_timeout'] != horizon['saved_player_idle_timeout'] or \
|
|
vanilla['saved_player_idle_timeout'] != '30':
|
|
failures.append(f"saved player idle timeout: vanilla {vanilla['saved_player_idle_timeout']!r}, Horizon {horizon['saved_player_idle_timeout']!r}")
|
|
if not horizon['saved_config_player_idle_timeout']:
|
|
failures.append('Horizon did not save player idle timeout in server.toml')
|
|
if vanilla['restarted_player_idle_timeout'] != horizon['restarted_player_idle_timeout'] or \
|
|
vanilla['restarted_player_idle_timeout'] != {'jsonrpc': '2.0', 'id': 133, 'result': 30}:
|
|
failures.append(f"restarted player idle timeout: vanilla {vanilla['restarted_player_idle_timeout']!r}, Horizon {horizon['restarted_player_idle_timeout']!r}")
|
|
if vanilla['saved_entity_broadcast_range'] != horizon['saved_entity_broadcast_range'] or \
|
|
vanilla['saved_entity_broadcast_range'] != '80':
|
|
failures.append(f"saved entity broadcast range: vanilla {vanilla['saved_entity_broadcast_range']!r}, Horizon {horizon['saved_entity_broadcast_range']!r}")
|
|
if not horizon['saved_config_entity_broadcast_range']:
|
|
failures.append('Horizon did not save entity broadcast range in server.toml')
|
|
if vanilla['restarted_entity_broadcast_range'] != horizon['restarted_entity_broadcast_range'] or \
|
|
vanilla['restarted_entity_broadcast_range'] != {'jsonrpc': '2.0', 'id': 189, 'result': 80}:
|
|
failures.append(f"restarted entity broadcast range: vanilla {vanilla['restarted_entity_broadcast_range']!r}, Horizon {horizon['restarted_entity_broadcast_range']!r}")
|
|
if vanilla['saved_simulation_distance'] != horizon['saved_simulation_distance'] or \
|
|
vanilla['saved_simulation_distance'] != '8':
|
|
failures.append(f"saved simulation distance: vanilla {vanilla['saved_simulation_distance']!r}, Horizon {horizon['saved_simulation_distance']!r}")
|
|
if not horizon['saved_config_simulation_distance']:
|
|
failures.append('Horizon did not save simulation distance in server.toml')
|
|
if vanilla['restarted_simulation_distance'] != horizon['restarted_simulation_distance'] or \
|
|
vanilla['restarted_simulation_distance'] != {'jsonrpc': '2.0', 'id': 179, 'result': 8}:
|
|
failures.append(f"restarted simulation distance: vanilla {vanilla['restarted_simulation_distance']!r}, Horizon {horizon['restarted_simulation_distance']!r}")
|
|
if vanilla['saved_view_distance'] != horizon['saved_view_distance'] or vanilla['saved_view_distance'] != '8':
|
|
failures.append(f"saved view distance: vanilla {vanilla['saved_view_distance']!r}, Horizon {horizon['saved_view_distance']!r}")
|
|
if not horizon['saved_config_view_distance']:
|
|
failures.append('Horizon did not save view distance in server.toml')
|
|
if vanilla['restarted_view_distance'] != horizon['restarted_view_distance'] or \
|
|
vanilla['restarted_view_distance'] != {'jsonrpc': '2.0', 'id': 169, 'result': 8}:
|
|
failures.append(f"restarted view distance: vanilla {vanilla['restarted_view_distance']!r}, Horizon {horizon['restarted_view_distance']!r}")
|
|
if vanilla['saved_spawn_protection'] != horizon['saved_spawn_protection'] or vanilla['saved_spawn_protection'] != '8':
|
|
failures.append(f"saved spawn protection: vanilla {vanilla['saved_spawn_protection']!r}, Horizon {horizon['saved_spawn_protection']!r}")
|
|
if not horizon['saved_config_spawn_protection']:
|
|
failures.append('Horizon did not save spawn protection in server.toml')
|
|
if vanilla['restarted_spawn_protection'] != horizon['restarted_spawn_protection'] or \
|
|
vanilla['restarted_spawn_protection'] != {'jsonrpc': '2.0', 'id': 159, 'result': 8}:
|
|
failures.append(f"restarted spawn protection: vanilla {vanilla['restarted_spawn_protection']!r}, Horizon {horizon['restarted_spawn_protection']!r}")
|
|
if vanilla['saved_operator_user_permission_level'] != horizon['saved_operator_user_permission_level'] or \
|
|
vanilla['saved_operator_user_permission_level'] != '2':
|
|
failures.append(f"saved operator permission level: vanilla {vanilla['saved_operator_user_permission_level']!r}, Horizon {horizon['saved_operator_user_permission_level']!r}")
|
|
if not horizon['saved_config_operator_user_permission_level']:
|
|
failures.append('Horizon did not save operator permission level in server.toml')
|
|
if vanilla['restarted_operator_user_permission_level'] != horizon['restarted_operator_user_permission_level'] or \
|
|
vanilla['restarted_operator_user_permission_level'] != {'jsonrpc': '2.0', 'id': 229, 'result': 2}:
|
|
failures.append(f"restarted operator permission level: vanilla {vanilla['restarted_operator_user_permission_level']!r}, Horizon {horizon['restarted_operator_user_permission_level']!r}")
|
|
if vanilla['saved_allow_flight'] != horizon['saved_allow_flight'] or vanilla['saved_allow_flight'] != 'true':
|
|
failures.append(f"saved allow-flight: vanilla {vanilla['saved_allow_flight']!r}, Horizon {horizon['saved_allow_flight']!r}")
|
|
if not horizon['saved_config_allow_flight']:
|
|
failures.append('Horizon did not save allow-flight in server.toml')
|
|
if vanilla['restarted_allow_flight'] != horizon['restarted_allow_flight'] or \
|
|
vanilla['restarted_allow_flight'] != {'jsonrpc': '2.0', 'id': 149, 'result': True}:
|
|
failures.append(f"restarted allow-flight: vanilla {vanilla['restarted_allow_flight']!r}, Horizon {horizon['restarted_allow_flight']!r}")
|
|
if vanilla['saved_accepts_transfers'] != horizon['saved_accepts_transfers'] or \
|
|
vanilla['saved_accepts_transfers'] != 'true':
|
|
failures.append(f"saved accepts-transfers: vanilla {vanilla['saved_accepts_transfers']!r}, Horizon {horizon['saved_accepts_transfers']!r}")
|
|
if not horizon['saved_config_accepts_transfers']:
|
|
failures.append('Horizon did not save accepts-transfers in server.toml')
|
|
if vanilla['restarted_accepts_transfers'] != horizon['restarted_accepts_transfers'] or \
|
|
vanilla['restarted_accepts_transfers'] != {'jsonrpc': '2.0', 'id': 308, 'result': True}:
|
|
failures.append(f"restarted accepts-transfers: vanilla {vanilla['restarted_accepts_transfers']!r}, Horizon {horizon['restarted_accepts_transfers']!r}")
|
|
if vanilla['saved_hide_online_players'] != horizon['saved_hide_online_players'] or \
|
|
vanilla['saved_hide_online_players'] != 'true':
|
|
failures.append(f"saved hide-online-players: vanilla {vanilla['saved_hide_online_players']!r}, Horizon {horizon['saved_hide_online_players']!r}")
|
|
if not horizon['saved_config_hide_online_players']:
|
|
failures.append('Horizon did not save hide-online-players in server.toml')
|
|
if vanilla['restarted_hide_online_players'] != horizon['restarted_hide_online_players'] or \
|
|
vanilla['restarted_hide_online_players'] != {'jsonrpc': '2.0', 'id': 199, 'result': True}:
|
|
failures.append(f"restarted hide-online-players: vanilla {vanilla['restarted_hide_online_players']!r}, Horizon {horizon['restarted_hide_online_players']!r}")
|
|
if vanilla['saved_status_replies'] != horizon['saved_status_replies'] or vanilla['saved_status_replies'] != 'false':
|
|
failures.append(f"saved status replies: vanilla {vanilla['saved_status_replies']!r}, Horizon {horizon['saved_status_replies']!r}")
|
|
if not horizon['saved_config_status_replies']:
|
|
failures.append('Horizon did not save status replies in server.toml')
|
|
if vanilla['restarted_status_replies'] != horizon['restarted_status_replies'] or \
|
|
vanilla['restarted_status_replies'] != {'jsonrpc': '2.0', 'id': 219, 'result': False}:
|
|
failures.append(f"restarted status replies: vanilla {vanilla['restarted_status_replies']!r}, Horizon {horizon['restarted_status_replies']!r}")
|
|
(build / 'report.json').write_text(json.dumps(sides, indent=1))
|
|
if failures:
|
|
sys.exit(f'{NAME}: {len(failures)} differences (logs in {build}):\n' + '\n'.join(failures))
|
|
report = lab.LOCAL / 'datagen/out/reports/json-rpc-api-schema.json'
|
|
discovered = horizon['answers'][MESSAGES.index('{"jsonrpc":"2.0","id":12,"method":"rpc.discover"}')]
|
|
if json.loads(report.read_text()) != discovered.get('result'):
|
|
failures.append(f'rpc.discover differs from {report}')
|
|
if failures:
|
|
sys.exit(f'{NAME}: {len(failures)} differences (logs in {build}):\n' + '\n'.join(failures))
|
|
finally:
|
|
shutil.rmtree(work, ignore_errors=True)
|
|
print(f'{NAME}: {len(MESSAGES) + 6} exchanges over TLS answered as vanilla answers them; rpc.discover matches '
|
|
'the report')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|