MCDR plugin: item share, hat, player head, calculator, join MOTD. All rights reserved. Author: 云镜之端/SKYMirror
254 lines
7.3 KiB
Python
254 lines
7.3 KiB
Python
"""Pure-function unit tests (no live MCDR server required)."""
|
|
import sys
|
|
import os
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from mcdreforged.api.all import Version
|
|
|
|
from iridium.calculator import CalcError, safe_eval
|
|
from iridium.hat import (
|
|
_components_to_command_inner,
|
|
_extract_head_item,
|
|
_hand_slot,
|
|
_head_slot,
|
|
_item_to_command_form,
|
|
_is_air,
|
|
)
|
|
from iridium.head import _give_head_command
|
|
from iridium.utils import (
|
|
extract_compound,
|
|
looks_like_item_snbt,
|
|
parse_selected_item,
|
|
sanitize_cmd,
|
|
sanitize_name,
|
|
strip_data_prefix,
|
|
)
|
|
|
|
|
|
def test_strip_prefix():
|
|
payload = 'Notch has the following entity data: {id:"minecraft:stone",Count:1b}'
|
|
assert strip_data_prefix(payload) == '{id:"minecraft:stone",Count:1b}'
|
|
|
|
|
|
def test_parse_selected_item():
|
|
nbt = '{SelectedItem:{id:"minecraft:diamond_sword",Count:1b,tag:{Damage:1}},SelectedItemSlot:0}'
|
|
assert parse_selected_item(nbt) == '{id:"minecraft:diamond_sword",Count:1b,tag:{Damage:1}}'
|
|
|
|
|
|
def test_extract_compound():
|
|
nbt = '{foo:1,SelectedItem:{id:"minecraft:apple",Count:3b},bar:2}'
|
|
assert extract_compound(nbt, "SelectedItem") == '{id:"minecraft:apple",Count:3b}'
|
|
|
|
|
|
def test_extract_list_field():
|
|
nbt = '{HandItems:[{id:"minecraft:stone",Count:1b},{}]}'
|
|
value = extract_compound(nbt, "HandItems")
|
|
assert value is not None
|
|
assert value.startswith("[")
|
|
assert "minecraft:stone" in value
|
|
|
|
|
|
def test_parse_hand_items_fallback():
|
|
nbt = '{HandItems:[{id:"minecraft:stone",Count:2b},{}]}'
|
|
item = parse_selected_item(nbt)
|
|
assert item == '{id:"minecraft:stone",Count:2b}'
|
|
|
|
|
|
def test_looks_like_item_snbt():
|
|
assert looks_like_item_snbt('{id:"minecraft:stone",Count:1b}')
|
|
assert not looks_like_item_snbt("No data found for SelectedItem")
|
|
assert not looks_like_item_snbt("")
|
|
assert not looks_like_item_snbt("{}")
|
|
|
|
|
|
def test_head_commands():
|
|
assert (
|
|
_give_head_command("Steve", "Notch", Version("1.20.5"))
|
|
== 'give Steve minecraft:player_head[minecraft:profile="Notch"] 1'
|
|
)
|
|
assert (
|
|
_give_head_command("Steve", "Notch", Version("1.16.5"))
|
|
== 'give Steve minecraft:player_head{SkullOwner:"Notch"} 1'
|
|
)
|
|
assert (
|
|
_give_head_command("Steve", "Notch", Version("1.12.2"))
|
|
== 'give Steve minecraft:skull 1 3 {SkullOwner:"Notch"}'
|
|
)
|
|
assert (
|
|
_give_head_command("Steve", 'Evil"name', Version("1.16.5"))
|
|
== 'give Steve minecraft:player_head{SkullOwner:"Evilname"} 1'
|
|
)
|
|
assert (
|
|
_give_head_command("Steve", 'Notch;give @a diamond', Version("1.16.5"))
|
|
== 'give Steve minecraft:player_head{SkullOwner:"Notchgiveadiamond"} 1'
|
|
)
|
|
# component / NBT breakout attempts
|
|
assert (
|
|
_give_head_command("Steve", 'alex"}{id:"minecraft:stone', Version("1.16.5"))
|
|
== 'give Steve minecraft:player_head{SkullOwner:"alexidminecraftstone"} 1'
|
|
)
|
|
assert (
|
|
_give_head_command("Steve", 'x[minecraft:profile=y]', Version("1.20.5"))
|
|
== 'give Steve minecraft:player_head[minecraft:profile="xminecraftprofiley"] 1'
|
|
)
|
|
# executor name is sanitized too
|
|
assert (
|
|
_give_head_command('Steve";give', "Notch", Version("1.16.5"))
|
|
== 'give Stevegive minecraft:player_head{SkullOwner:"Notch"} 1'
|
|
)
|
|
# length cap
|
|
assert (
|
|
_give_head_command("Steve", "A" * 100, Version("1.16.5"))
|
|
== 'give Steve minecraft:player_head{SkullOwner:"' + "A" * 32 + '"} 1'
|
|
)
|
|
|
|
|
|
def test_item_form_modern():
|
|
part, count = _item_to_command_form(
|
|
'{id:"minecraft:diamond",count:2,components:{"minecraft:custom_name":"x"}}',
|
|
Version("1.20.5"),
|
|
)
|
|
assert part == 'minecraft:diamond[minecraft:custom_name="x"]'
|
|
assert count == 2
|
|
|
|
|
|
def test_components_to_command_inner():
|
|
inner = _components_to_command_inner(
|
|
'{"minecraft:custom_name":{text:"hi"}, "minecraft:damage":1, "minecraft:enchantments":{}}'
|
|
)
|
|
assert 'minecraft:custom_name={text:"hi"}' in inner
|
|
assert "minecraft:damage=1" in inner
|
|
assert "minecraft:enchantments={}" in inner
|
|
|
|
|
|
def test_item_form_legacy():
|
|
part, count = _item_to_command_form(
|
|
'{id:"minecraft:stone",Count:1b,tag:{display:{Name:"hi"}}}',
|
|
Version("1.12.2"),
|
|
)
|
|
assert part == 'minecraft:stone{display:{Name:"hi"}}'
|
|
assert count == 1
|
|
|
|
|
|
def test_slots():
|
|
assert _head_slot(Version("1.20.1")) == "armor.head"
|
|
assert _head_slot(Version("1.12.2")) == "slot.armor.head"
|
|
assert _hand_slot(Version("1.20.1")) == "weapon.mainhand"
|
|
assert _hand_slot(Version("1.12.2")) == "slot.weapon"
|
|
assert _hand_slot(Version("1.8.9"), 3) == "slot.hotbar.3"
|
|
assert _hand_slot(Version("1.8.9")) == "slot.hotbar.0"
|
|
|
|
|
|
def test_head_extract_1_12():
|
|
payload = (
|
|
'Steve has the following entity data: '
|
|
'{SelectedItem:{id:"minecraft:stone",Count:1b},'
|
|
'Inventory:[{Slot:103b,id:"minecraft:iron_helmet",Count:1b},{Slot:0b,id:"minecraft:dirt",Count:1b}]}'
|
|
)
|
|
item = _extract_head_item(payload, Version("1.12.2"))
|
|
assert item is not None
|
|
assert "iron_helmet" in item
|
|
|
|
|
|
def test_is_air():
|
|
assert _is_air(None)
|
|
assert _is_air("{}")
|
|
assert _is_air('{id:"minecraft:air"}')
|
|
assert not _is_air('{id:"minecraft:stone",Count:1b}')
|
|
|
|
|
|
def test_calculator():
|
|
assert safe_eval("1+2*3") == 7
|
|
assert safe_eval("(1+2)*(3-1)") == 6
|
|
assert abs(safe_eval("10/4") - 2.5) < 1e-9
|
|
assert safe_eval("2**10") == 1024
|
|
assert safe_eval("2^10") == 1024
|
|
try:
|
|
safe_eval("__import__('os')")
|
|
raise AssertionError("should reject")
|
|
except CalcError:
|
|
pass
|
|
try:
|
|
safe_eval("1;print(1)")
|
|
raise AssertionError("should reject")
|
|
except CalcError:
|
|
pass
|
|
|
|
|
|
class _DummyServer:
|
|
def get_server_information(self):
|
|
return None
|
|
|
|
|
|
def test_motd_days_and_links():
|
|
from datetime import datetime
|
|
|
|
from iridium import join_tip
|
|
from iridium.config import Config, apply_config
|
|
|
|
cfg = Config()
|
|
cfg.motd_start_day = "2020-01-01"
|
|
cfg.motd_enabled = True
|
|
apply_config(cfg)
|
|
days = join_tip.server_days()
|
|
assert days is not None
|
|
assert days == (datetime.now() - datetime(2020, 1, 1)).days + 1
|
|
|
|
pieces = join_tip.render_motd_line(
|
|
"§e欢迎 §f{player}§e,官网 {link:§b打开|https://example.com}",
|
|
"Steve",
|
|
_DummyServer(),
|
|
)
|
|
assert pieces
|
|
joined = "".join(p.to_plain_text() for p in pieces)
|
|
assert "Steve" in joined
|
|
assert "打开" in joined
|
|
|
|
|
|
def test_motd_placeholder_replacement():
|
|
from iridium import join_tip
|
|
from iridium.config import Config, apply_config
|
|
|
|
cfg = Config()
|
|
cfg.motd_start_day = ""
|
|
cfg.motd_lines = ["hi {player} / {days} / {online}"]
|
|
apply_config(cfg)
|
|
text = join_tip.replace_placeholders("hi {player} / {days} / {online}", "Alex", _DummyServer())
|
|
assert text == "hi Alex / ? / ?"
|
|
|
|
|
|
def test_sanitize_name_and_cmd():
|
|
assert sanitize_name("Steve") == "Steve"
|
|
assert sanitize_name('Evil";give') == "Evilgive"
|
|
assert sanitize_name("A" * 100) == "A" * 32
|
|
assert sanitize_name("") == ""
|
|
assert sanitize_cmd("give x\ngive y") == "give x give y"
|
|
assert sanitize_cmd("a\r\nb") == "a b"
|
|
|
|
|
|
def test_share_query_sanitizes_player():
|
|
from mcdreforged.api.all import Version
|
|
|
|
from iridium.share import _build_tellraw, _query_command
|
|
|
|
cmd = _query_command('Steve";op', Version("1.16.5"))
|
|
assert 'Steveop' in cmd
|
|
assert '";' not in cmd
|
|
tr = _build_tellraw('Bad"name', '{id:"minecraft:stone",Count:1b}', Version("1.20.1"))
|
|
assert "Badname" in tr
|
|
|
|
|
|
if __name__ == "__main__":
|
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
|
failed = 0
|
|
for fn in fns:
|
|
try:
|
|
fn()
|
|
print(f"PASS {fn.__name__}")
|
|
except Exception as e:
|
|
failed += 1
|
|
print(f"FAIL {fn.__name__}: {e}")
|
|
print(f"\n{len(fns) - failed}/{len(fns)} passed")
|
|
sys.exit(1 if failed else 0)
|