"""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 解析:全部通过")