176 lines
7.5 KiB
Python
176 lines
7.5 KiB
Python
"""实例层回归:传入文件夹 → 拉起实例(版本 + mod + 方块表)。"""
|
||
import sys, os, json, zipfile, tempfile, shutil
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from mcfold.instance import Instance
|
||
from mcfold.modjar import scan_mods
|
||
|
||
BLOCKS_MOD = "/home/minecraft/mods/[扩充乐事] expandeddelight-0.1.4.jar"
|
||
FORGE_MOD = "/home/mc-test/mods/curtain-mc1.20.1-1.3.2.jar"
|
||
REAL_INST = "/home/newqqagent/mcview/store/vanilla-1.20.1"
|
||
|
||
def build_instance(with_mods=True, version_id="1.20.1", mc=None):
|
||
"""造一个临时实例文件夹(完整 MC 客户端安装形状)。
|
||
|
||
version_id 是目录名(可能是加载器 id);mc 是它继承的原版版本。
|
||
"""
|
||
d = tempfile.mkdtemp(prefix="mcfold-inst-")
|
||
ver = version_id
|
||
vdir = os.path.join(d, "versions", ver)
|
||
os.makedirs(vdir, exist_ok=True)
|
||
obj = {"id": ver, "mainClass": "net.minecraft.client.main.Main", "assets": "5"}
|
||
if mc and mc != ver:
|
||
obj["inheritsFrom"] = mc
|
||
obj["mainClass"] = ("io.github.zekerzhayard.forgewrapper.installer.Main"
|
||
if "forge" in ver else
|
||
"net.fabricmc.loader.impl.launch.knot.KnotClient")
|
||
with open(os.path.join(vdir, f"{ver}.json"), "w") as f:
|
||
json.dump(obj, f)
|
||
os.makedirs(os.path.join(d, "mods"), exist_ok=True)
|
||
if with_mods:
|
||
for src in (BLOCKS_MOD, FORGE_MOD):
|
||
if os.path.isfile(src):
|
||
shutil.copy(src, os.path.join(d, "mods"))
|
||
return d
|
||
|
||
def test_load_from_folder():
|
||
"""★ 用户口径:传入文件夹路径 → 拉起实例。"""
|
||
d = build_instance()
|
||
try:
|
||
inst = Instance(d).load()
|
||
st = inst.status()
|
||
assert st.mc_version == "1.20.1", st.mc_version
|
||
assert st.protocol == 763, st.protocol
|
||
assert st.loader == "vanilla", st.loader
|
||
print(f" ✓ 传入文件夹拉起实例: {st.version_id} mc={st.mc_version} 协议={st.protocol}")
|
||
finally:
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
|
||
def test_mods_registered():
|
||
"""★ 读取 mod jar 注册方块表。"""
|
||
d = build_instance()
|
||
try:
|
||
inst = Instance(d).load()
|
||
ms = inst.mods_summary()
|
||
assert ms["loaded"] == 2, ms
|
||
assert ms["block_name_count"] > 20, ms
|
||
names = inst.mod_block_names()
|
||
assert "expandeddelight:cask" in names, names[:5]
|
||
# 诚实边界必须体现在摘要里
|
||
assert "state id" in ms["note"]
|
||
print(f" ✓ mod 注册: {ms['loaded']} 个 mod, {ms['block_name_count']} 个方块名")
|
||
finally:
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
|
||
def test_loader_version_normalization():
|
||
"""forge 实例:mc_version 必须归一化(否则协议选错)。"""
|
||
d = build_instance(version_id="1.20.1-forge-47.4.0", mc="1.20.1")
|
||
try:
|
||
inst = Instance(d, prefer_version="1.20.1-forge-47.4.0").load()
|
||
st = inst.status()
|
||
assert st.mc_version == "1.20.1", st.mc_version
|
||
assert st.loader == "forge", st.loader
|
||
assert st.protocol == 763, st.protocol
|
||
print(f" ✓ forge 归一化: id={st.version_id} → mc={st.mc_version} 协议={st.protocol}")
|
||
finally:
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
|
||
def test_block_name_validation():
|
||
"""方块名校验:裸名可展开;歧义不误判。"""
|
||
d = build_instance()
|
||
try:
|
||
inst = Instance(d).load()
|
||
assert inst.knows_block_name("cask")
|
||
assert inst.expand_block_name("cask") == "expandeddelight:cask"
|
||
assert inst.knows_block_name("stone")
|
||
assert not inst.knows_block_name("definitely_not_a_block")
|
||
print(" ✓ 方块名校验: 裸名展开正确, 不存在的方块判否")
|
||
finally:
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
|
||
def test_ambiguous_bare_name():
|
||
"""两个 mod 有同名方块时,裸名必须判为**不可用**(歧义)。"""
|
||
d = build_instance(with_mods=False)
|
||
try:
|
||
for mid in ("moda", "modb"):
|
||
with zipfile.ZipFile(os.path.join(d, "mods", f"{mid}.jar"), "w") as zf:
|
||
zf.writestr("fabric.mod.json",
|
||
json.dumps({"id": mid, "name": mid, "version": "1"}))
|
||
zf.writestr(f"assets/{mid}/blockstates/shared_block.json", "{}")
|
||
inst = Instance(d).load()
|
||
assert not inst.knows_block_name("shared_block"), "有歧义却判为认识"
|
||
assert inst.knows_block_name("moda:shared_block"), "限定名应可用"
|
||
print(" ✓ 歧义处理: 裸名(两 mod 同名)判否, 限定名可判是")
|
||
finally:
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
|
||
def test_real_install_folder():
|
||
"""对真实安装目录验证(无 mod 的 vanilla)。"""
|
||
if not os.path.isdir(REAL_INST):
|
||
print(" - 真实安装目录不存在,跳过"); return
|
||
inst = Instance(REAL_INST).load()
|
||
st = inst.status()
|
||
assert st.mc_version == "1.20.1" and st.protocol == 763, (st.mc_version, st.protocol)
|
||
assert inst.registry is not None and len(inst.registry.blocks) > 900
|
||
print(f" ✓ 真实安装: {st.version_id} 协议={st.protocol} 原版方块={len(inst.registry.blocks)}")
|
||
|
||
def test_resolve_block_name_mod_attribution():
|
||
"""越界 state id 的报错必须**归因到 mod**(让降级可信)。"""
|
||
d = build_instance()
|
||
try:
|
||
inst = Instance(d).load()
|
||
msg = inst.resolve_block_name(99999)
|
||
assert msg.startswith("unknown("), msg
|
||
assert "mod" in msg, msg
|
||
print(f" ✓ 越界归因: {msg[:46]}…")
|
||
finally:
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
|
||
def test_e2e_connect_from_instance():
|
||
"""★ 端到端:实例文件夹 → 连服 → 登录(协议由文件夹决定,不硬编码)。"""
|
||
import socket, subprocess, time
|
||
HOST, PORT = "127.0.0.1", 25599
|
||
runtime = "/home/newqqagent/plugin_data/mc/runtime"
|
||
def reachable():
|
||
try:
|
||
socket.create_connection((HOST, PORT), timeout=2).close(); return True
|
||
except OSError:
|
||
return False
|
||
proc = None
|
||
if not reachable():
|
||
src = os.path.join(os.path.dirname(os.path.abspath(__file__)), "support_testserver.js")
|
||
if not os.path.isfile(src):
|
||
print(" - 无 support_testserver.js,跳过"); return
|
||
shutil.copy(src, runtime)
|
||
proc = subprocess.Popen(["node", "support_testserver.js", str(PORT)], cwd=runtime,
|
||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||
for _ in range(50):
|
||
if reachable(): break
|
||
time.sleep(0.2)
|
||
if not reachable():
|
||
print(" - 服务端不可用,跳过")
|
||
if proc: proc.kill()
|
||
return
|
||
d = build_instance()
|
||
try:
|
||
inst = Instance(d).load()
|
||
c = inst.connect(HOST, PORT, "McfoldBot")
|
||
r = c.run_smoke()
|
||
assert r["protocol"] == 763 and r["login_ok"], r
|
||
print(f" ✓ 端到端: 文件夹→协议{r['protocol']}→登录成功 (mod {len(inst.mods)} 个)")
|
||
c.close()
|
||
finally:
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
if proc:
|
||
proc.terminate()
|
||
try: proc.wait(timeout=5)
|
||
except Exception: proc.kill()
|
||
try: os.remove(os.path.join(runtime, "support_testserver.js"))
|
||
except OSError: pass
|
||
|
||
if __name__ == "__main__":
|
||
for fn in [test_load_from_folder, test_mods_registered, test_loader_version_normalization,
|
||
test_block_name_validation, test_ambiguous_bare_name, test_real_install_folder,
|
||
test_resolve_block_name_mod_attribution, test_e2e_connect_from_instance]:
|
||
fn()
|
||
print("\n实例层:全部通过")
|