feat(mcfold): P5 mod jar 解析(三种加载器+多加载器诊断+方块名提取,含诚实边界标注)
This commit is contained in:
126
tests/test_modjar.py
Normal file
126
tests/test_modjar.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""mod jar 解析回归 —— 用**真实 mod jar** 验证(三种加载器 + 边界)。"""
|
||||
import sys, os, json, zipfile, io, tempfile
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from mcfold.modjar import parse_mod, scan_mods, summarize, _parse_toml, detect_loaders
|
||||
|
||||
FORGE = "/home/mc-test/mods/curtain-mc1.20.1-1.3.2.jar"
|
||||
NEOFORGE= "/home/minecraft/mods/bettercombat-neoforge-2.3.2+1.21.1.jar"
|
||||
MULTI = "/home/minecraft/mods/Explorify v1.6.4 f15-88.mod.jar"
|
||||
BLOCKS = "/home/minecraft/mods/[扩充乐事] expandeddelight-0.1.4.jar"
|
||||
|
||||
def exists(p): return os.path.isfile(p)
|
||||
|
||||
def test_toml_parser_comment_headers():
|
||||
"""★ 踩过的坑:表头带行内注释('[[mods]] #mandatory')必须正确识别。"""
|
||||
text = '''
|
||||
loaderVersion = "[47,)"
|
||||
[[mods]] #mandatory
|
||||
modId = "curtain" #mandatory
|
||||
version = "${file.jarVersion}"
|
||||
displayName = "Curtain"
|
||||
[[dependencies.curtain]]
|
||||
modId = "forge"
|
||||
'''
|
||||
m = _parse_toml(text)
|
||||
assert len(m["mods"]) == 1, f"mods 段数应为 1,实际 {len(m['mods'])}"
|
||||
assert m["mods"][0]["modId"] == "curtain", m["mods"][0]
|
||||
assert m["mods"][0]["displayName"] == "Curtain"
|
||||
# 依赖段的 modId 不应污染 mods[0]
|
||||
assert m["mods"][0]["modId"] != "forge"
|
||||
print(" ✓ TOML: 带行内注释的 [[mods]] 正确识别,依赖段不污染")
|
||||
|
||||
def test_forge_real():
|
||||
if not exists(FORGE):
|
||||
print(" - forge 样本不存在,跳过"); return
|
||||
m = parse_mod(FORGE)
|
||||
assert m.loader == "forge", m.loader
|
||||
assert m.mod_id == "curtain", m.mod_id
|
||||
assert m.name == "Curtain", m.name
|
||||
# curtain 只加命令、不新增方块 —— 0 方块是正确结果
|
||||
assert m.block_names == [], m.block_names
|
||||
print(f" ✓ Forge 真实: id={m.mod_id} name={m.name} 方块=0(它只加命令)")
|
||||
|
||||
def test_neoforge_real():
|
||||
if not exists(NEOFORGE):
|
||||
print(" - neoforge 样本不存在,跳过"); return
|
||||
m = parse_mod(NEOFORGE)
|
||||
assert m.loader == "neoforge", m.loader
|
||||
assert m.mod_id == "bettercombat", m.mod_id
|
||||
assert m.version == "2.3.2+1.21.1", m.version
|
||||
print(f" ✓ NeoForge 真实: id={m.mod_id} ver={m.version}")
|
||||
|
||||
def test_multi_loader_jar():
|
||||
"""多加载器 jar(同时含 forge/neoforge/fabric 元数据)必须能诊断。"""
|
||||
if not exists(MULTI):
|
||||
print(" - 多加载器样本不存在,跳过"); return
|
||||
m = parse_mod(MULTI)
|
||||
assert len(m.loaders_present) >= 2, m.loaders_present
|
||||
assert m.mod_id == "explorify", m.mod_id
|
||||
# 结构 mod:用原版方块生成地形,自己不新增方块 ⇒ 0 方块正确
|
||||
assert m.block_names == [], m.block_names
|
||||
print(f" ✓ 多加载器 jar: 含 {m.loaders_present},id={m.mod_id}(结构 mod,0 方块)")
|
||||
|
||||
def test_block_extraction_real():
|
||||
"""★ 核心:从真实 mod 提取方块名清单(这是 mod 支持的可见部分)。"""
|
||||
if not exists(BLOCKS):
|
||||
print(" - 方块 mod 样本不存在,跳过"); return
|
||||
m = parse_mod(BLOCKS)
|
||||
assert m.mod_id == "expandeddelight", m.mod_id
|
||||
assert len(m.block_names) > 20, f"方块数偏少: {len(m.block_names)}"
|
||||
# 已知方块(前面实测过)
|
||||
for want in ("cask", "cheese_wheel", "asparagus_crate"):
|
||||
assert want in m.block_names, f"缺方块 {want}"
|
||||
q = m.qualified_blocks()
|
||||
assert "expandeddelight:cask" in q, q[:5]
|
||||
# 物品也应提取到
|
||||
assert len(m.item_names) > 20, len(m.item_names)
|
||||
print(f" ✓ 方块提取: {len(m.block_names)} 方块 / {len(m.item_names)} 物品,含 cask/cheese_wheel")
|
||||
|
||||
def test_no_state_id_claim():
|
||||
"""★ 诚实性:解析结果里**不含** state id(因为拿不到),摘要必须明说。"""
|
||||
m = parse_mod(BLOCKS) if exists(BLOCKS) else None
|
||||
if m is None:
|
||||
print(" - 跳过"); return
|
||||
s = summarize([m])
|
||||
assert "state id" in s["note"], s["note"]
|
||||
assert "block_names" in s and s["block_name_count"] > 0
|
||||
# 摘要里不应有任何形如 state id 的映射承诺
|
||||
assert "state_ids" not in s and "stateId" not in json.dumps(s)
|
||||
print(f" ✓ 诚实标注: 摘要含 {s['block_name_count']} 方块名,并注明无 state id")
|
||||
|
||||
def test_bad_jar_no_crash():
|
||||
"""损坏的 jar 不能中断整目录扫描。"""
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = os.path.join(d, "broken.jar")
|
||||
open(p, "wb").write(b"this is not a zip")
|
||||
m = parse_mod(p)
|
||||
assert not m.ok and "zip" in (m.error or "").lower(), m.error
|
||||
good = os.path.join(d, "ok.jar")
|
||||
with zipfile.ZipFile(good, "w") as zf:
|
||||
zf.writestr("fabric.mod.json", json.dumps({"id":"t","name":"T","version":"1"}))
|
||||
mods = scan_mods(d)
|
||||
assert len(mods) == 2, len(mods)
|
||||
s = summarize(mods)
|
||||
assert s["loaded"] == 1 and s["failed"] == 1, s
|
||||
print(f" ✓ 损坏 jar 不中断扫描: loaded={s['loaded']} failed={s['failed']}")
|
||||
|
||||
def test_json_meta_without_assets():
|
||||
"""纯 JSON 元数据、无 assets 的 mod 也要能拿到 id(边界)。"""
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = os.path.join(d, "t.jar")
|
||||
with zipfile.ZipFile(p, "w") as zf:
|
||||
zf.writestr("fabric.mod.json", json.dumps(
|
||||
{"id":"mymod","name":"My Mod","version":"2.0",
|
||||
"depends":{"minecraft":">=1.20","fabric-api":"*"}}))
|
||||
m = parse_mod(p)
|
||||
assert m.loader == "fabric" and m.mod_id == "mymod", (m.loader, m.mod_id)
|
||||
assert m.depends.get("minecraft") == ">=1.20", m.depends
|
||||
assert m.block_names == []
|
||||
print(" ✓ fabric 纯元数据: id/depends 正确,无 assets 不报错")
|
||||
|
||||
if __name__ == "__main__":
|
||||
for fn in [test_toml_parser_comment_headers, test_forge_real, test_neoforge_real,
|
||||
test_multi_loader_jar, test_block_extraction_real, test_no_state_id_claim,
|
||||
test_bad_jar_no_crash, test_json_meta_without_assets]:
|
||||
fn()
|
||||
print("\nmod jar 解析:全部通过")
|
||||
Reference in New Issue
Block a user