874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
135 lines
7.8 KiB
Python
135 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare the chat screen's input with the vanilla client's.
|
|
|
|
Each case opens the chat screen (empty or with "/", a GUI size, the sent
|
|
message history, automatic suggestions on or off, the chat background
|
|
option) and types, presses keys (editing, selection, word moves, select
|
|
all, history up and down, Tab and Shift+Tab through suggestions, Escape)
|
|
and clicks (on the suggestion list, on the input). The Java oracle
|
|
(VanillaChatScreenOracle) drives a real ChatScreen, whose CommandSuggestions
|
|
parse with vanilla's client command tree and ask the server's dispatcher
|
|
for the suggestions a node leaves to it; examples/probe/chat_screen_fixture runs
|
|
horizon_client::chat::ChatScreen on Horizon's dispatcher. After the actions
|
|
the input's value, cursor, selection and scroll and the screen's GUI calls
|
|
(the input background, the EditBox with its argument colouring, cursor,
|
|
selection and suggestion text, then the suggestion list or the usage lines)
|
|
must agree.
|
|
|
|
test_chat_screen_parity.py [--count N] [--detail N]
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import random
|
|
import subprocess
|
|
import sys
|
|
|
|
try:
|
|
from . import interaction_lab as lib, lab, test_rust_parity as base, test_model_baking_parity as baking
|
|
except ImportError:
|
|
import interaction_lab as lib
|
|
import lab
|
|
import test_rust_parity as base
|
|
import test_model_baking_parity as baking
|
|
|
|
PARITY = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity'
|
|
TEXTS = ['/give @p minecraft:dia', '/tp ', '/gamemode cr', 'hello world', '/effect give @s ', '/execute as @a run ',
|
|
'/time set da', '/xyz', '/give Steve stone 64 extra', '/tell Al', '/scoreboard objectives add k dummy',
|
|
'/summon minecraft:zom', '/setblock ~ ~1 ~ minecraft:oak_stairs[facing=', '/team join r', '/kill @e[type=',
|
|
'/', 'a', ' ', 'S', '@', '~ ', 'minecraft:', '/weather ', '/say this is a long message that goes on and on and on '
|
|
'until it no longer fits the chat box', '/particle ', '/enchant @s sharp', '/difficulty ', '/gamerule ',
|
|
'/attribute @s ', '/fill 0 0 0 1 1 1 stone repl', '/clear @p diamond 3', '/locate structure vil']
|
|
KEYS = ['tab', 'tab', 'tab', 'shift+tab', 'up', 'down', 'left', 'right', 'home', 'end', 'backspace', 'backspace',
|
|
'ctrl+backspace', 'delete', 'ctrl+left', 'ctrl+right', 'shift+left', 'shift+right', 'shift+home', 'ctrl+a', 'escape']
|
|
HISTORY = ['/time set day', 'hi', '/give @p diamond 64', '/gamemode creative', 'a longer message in the history', '/tp ~ ~10 ~']
|
|
|
|
|
|
def cases(count):
|
|
rng = random.Random(0x43484154)
|
|
out = []
|
|
for _ in range(count):
|
|
width, height = rng.choice([(320, 240), (427, 240), (480, 270), (640, 360), (854, 480), (240, 180)])
|
|
actions = []
|
|
for _ in range(rng.choice([1, 2, 3, 5, 8, 12])):
|
|
r = rng.random()
|
|
if r < 0.45:
|
|
actions.append({'type': 'type', 'text': rng.choice(TEXTS)})
|
|
elif r < 0.85:
|
|
key = rng.choice(KEYS)
|
|
if key == 'escape' and rng.random() < 0.7:
|
|
key = 'tab'
|
|
actions.append({'type': 'key', 'key': key})
|
|
else:
|
|
x = rng.randint(0, width // 2)
|
|
y = rng.choice([rng.randint(height - 15 - 120, height - 15), rng.randint(height - 14, height - 1)])
|
|
actions.append({'type': 'click', 'x': x, 'y': y})
|
|
mouse = rng.choice([(rng.randint(0, width - 1), rng.randint(0, height - 1)),
|
|
(rng.randint(0, width // 2), rng.randint(height - 15 - 120, height - 15)), (0, 0)])
|
|
out.append({'width': width, 'height': height, 'initial': rng.choice(['', '', '', '/']),
|
|
'history': rng.sample(HISTORY, rng.randint(0, len(HISTORY))), 'autoSuggest': rng.random() < 0.85,
|
|
'chatOnly': rng.random() < 0.7, 'opacity': rng.choice([0.0, 0.5, 1.0]), 'actions': actions,
|
|
'mouse': list(mouse), 'blink': rng.choice([0, 100, 400, 650])})
|
|
# Always exercise registry-backed suggestions and their replacement ranges,
|
|
# before and after Tab, including the redirect through execute's root.
|
|
for text in ['/give @s diamond_sw', '/setblock ~ ~ ~ sto', '/fill ~ ~ ~ ~1 ~1 ~1 sto',
|
|
'/setblock ~ ~ ~ oak_stairs[fa', '/clear @s diamond_sw',
|
|
'/execute run give @s diamond_sw', '/fill ']:
|
|
for keys in [[], ['tab'], ['tab', 'tab', 'shift+tab']]:
|
|
out.append({'width': 640, 'height': 360, 'initial': '', 'history': [], 'autoSuggest': True,
|
|
'chatOnly': True, 'opacity': 0.5, 'mouse': [0, 0], 'blink': 0,
|
|
'actions': [{'type': 'type', 'text': text}]
|
|
+ [{'type': 'key', 'key': key} for key in keys]})
|
|
return out
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--count', type=int, default=3000)
|
|
parser.add_argument('--detail', type=int, default=3)
|
|
args = parser.parse_args()
|
|
lock, jars, _ = base.verify_artifacts()
|
|
libraries = baking.client_libraries()
|
|
build = lab.LOCAL / 'chat-screen-parity'; build.mkdir(parents=True, exist_ok=True)
|
|
chosen = cases(args.count)
|
|
inputs = build / 'inputs.jsonl'; inputs.write_text(''.join(json.dumps(c) + '\n' for c in chosen))
|
|
classes = build / 'classes'; classes.mkdir(exist_ok=True)
|
|
dependencies = os.pathsep.join(map(str, [jars['client'], *libraries]))
|
|
|
|
def run(command, name):
|
|
with (build / (name + '.log')).open('w') as log:
|
|
result = subprocess.run(list(map(str, command)), cwd=lab.ROOT, timeout=3600, stdout=log, stderr=subprocess.STDOUT)
|
|
if result.returncode:
|
|
print((build / (name + '.log')).read_text()[-4000:]); sys.exit(f'{name} failed')
|
|
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-nowarn', '-cp', dependencies, '-d', classes,
|
|
*(PARITY / f'{n}.java' for n in ('ParityData', 'ParityFont', 'VanillaPackScreenOracle', 'VanillaCommandParseOracle',
|
|
'VanillaChatScreenOracle'))], 'compile-java')
|
|
java = ['java', '-Xmx2G', '-cp', str(classes) + os.pathsep + dependencies]
|
|
run([*java, 'dev.horizonlab.parity.VanillaCommandParseOracle', 'registries', build], 'registries')
|
|
run([*java, 'dev.horizonlab.parity.VanillaChatScreenOracle', inputs, build / 'vanilla.jsonl'], 'vanilla')
|
|
lib.cargo_example('horizon-client', 'chat_screen_fixture', lib.RESOURCES, build / 'registries.json', inputs, build / 'rust.jsonl')
|
|
vanilla = [json.loads(l) for l in (build / 'vanilla.jsonl').read_text().splitlines()]
|
|
rust = [json.loads(l) for l in (build / 'rust.jsonl').read_text().splitlines()]
|
|
assert len(vanilla) == len(rust) == len(chosen), (len(vanilla), len(rust), len(chosen))
|
|
errors = [v['error'] for v in vanilla if 'error' in v]
|
|
bad = [(c, v, r) for c, v, r in zip(chosen, vanilla, rust) if 'error' not in v and v != r]
|
|
for e in sorted(set(errors))[:5]:
|
|
print('ORACLE ERROR', e[:1500])
|
|
for c, v, r in bad[:args.detail]:
|
|
print('MISMATCH', json.dumps({k: c[k] for k in c if k != 'history'})[:1500])
|
|
for k in ('closed', 'value', 'cursor', 'highlight', 'display'):
|
|
if v.get(k) != r.get(k):
|
|
print(f' {k}: vanilla {v.get(k)!r} | horizon {r.get(k)!r}')
|
|
a, b = v.get('draws', []), r.get('draws', [])
|
|
i = next((i for i in range(max(len(a), len(b))) if i >= len(a) or i >= len(b) or a[i] != b[i]), None)
|
|
if i is not None:
|
|
print(f' call {i}: vanilla {json.dumps(a[i]) if i < len(a) else None}')
|
|
print(f' call {i}: horizon {json.dumps(b[i]) if i < len(b) else None}')
|
|
print(f'{len(chosen) - len(bad) - len(errors)}/{len(chosen)} chat screens match; {len(errors)} oracle errors')
|
|
if bad or errors:
|
|
sys.exit(1)
|
|
print('chat screen parity ok')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|