diff --git a/tests/test_render_bridge.py b/tests/test_render_bridge.py index 2c0052d..04db617 100644 --- a/tests/test_render_bridge.py +++ b/tests/test_render_bridge.py @@ -20,6 +20,14 @@ from mcfold.render import SECTION_VOLUME, WorldRenderer, parse_save_chunk SAVE = Path("/home/mc-test/mc-test") STORE = Path("/home/newqqagent/mcview/store/vanilla-1.20.1") +# ★ 实例目录是 store/ 的**兄弟**(不是嵌套在里面)。 +# 权威口径:mcview/lib/versions.cjs:59/61 +# contentRoot = BASE/store/ (共享只读:jar/libraries/assets) +# instanceDir = BASE/instances/(实例独占:mods/config/options/saves) +# store 里再开一层 instances/ 会让「一个版本的内容根」多出实例态, +# 正是版本隔离要防的那种回流。 +INSTANCES = Path("/home/newqqagent/mcview/instances") +TEST_INSTANCE = INSTANCES / "test-mcfold" def _available() -> tuple[bool, str]: @@ -42,7 +50,7 @@ def _open_backend(): return None, why br = Gen_1_20( # ★ 用测试专用实例目录,不碰生产 default(版本隔离策略) - game_dir=str(STORE / "instances" / "test-mcfold"), + game_dir=str(TEST_INSTANCE), assets=str(STORE / "assets"), asset_index="1.20.1", ) try: diff --git a/tests/test_version_isolation.py b/tests/test_version_isolation.py new file mode 100644 index 0000000..de30a77 --- /dev/null +++ b/tests/test_version_isolation.py @@ -0,0 +1,111 @@ +"""版本隔离守卫:实例目录与内容根必须同级,实例态不得回流进内容根。 + +权威口径(`mcview/lib/versions.cjs:59/61`): + + contentRoot = BASE/store/- 共享只读:jar / libraries / assets + instanceDir = BASE/instances/ 实例独占:mods / config / options / saves + runtimeDir = BASE/runtime/ 实例独占:X display / screenshots / logs + +为什么这条值得单独一个文件 +-------------------------- +它**不需要 JVM**(纯路径断言,毫秒级),所以必须落在默认测试里 —— +放进行 `--with-render` 才跑的套件等于没守卫(大多数人不会加那个参数)。 + +★ 这条曾经真的被踩过:渲染联测把实例写成 `/instances/`, + 于是「一个版本的内容根」里长出了实例态。而版本隔离要防的崩因正是 + 「多实例共用 mods」—— 内层目录会把它重新引回来,而那条路径**看起来完全合法**。 + 所以检查的不是「路径能不能用」,而是「它违反了哪条分层」。 +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +BASE = Path("/home/newqqagent/mcview") +STORE = BASE / "store" +INSTANCES = BASE / "instances" +RUNTIME = BASE / "runtime" + +# 测试会真正使用的实例(新增测试实例时加到这里,守卫才会管到它) +TEST_INSTANCES = ["test-mcfold", "exp-vanilla-1.20.1", "exp-loader-1201"] + + +def test_three_layers_are_siblings(): + """三层(store / instances / runtime)是 BASE 下的**兄弟**,不是嵌套。""" + for d in (STORE, INSTANCES, RUNTIME): + assert d.parent == BASE, f"{d} 应在 {BASE} 直下,实际父目录 {d.parent}" + assert STORE != INSTANCES and INSTANCES != RUNTIME + print(f" ✓ 三层同级({STORE.name}/ · {INSTANCES.name}/ · {RUNTIME.name}/)") + + +def test_no_instance_state_inside_content_roots(): + """★ 内容根里不得出现实例态(instances/ 或独占文件)。 + + 这是本条守卫的核心:内容根是**共享只读**的,一旦有实例独占物落进来, + 下一个实例就会读到它 —— 而那正是「共用 mods」类崩因的开端。 + """ + if not STORE.is_dir(): + print(" - 跳过(无 store/)") + return + offenders = [] + for root in sorted(p for p in STORE.iterdir() if p.is_dir()): + if (root / "instances").exists(): + offenders.append(f"{root}/instances/(内容根里嵌套了实例层)") + # 实例独占物(凡出现即说明有实例把内容根当成自己的家) + for name in ("mods", "config", "options.txt", "saves"): + p = root / name + if p.exists(): + offenders.append(f"{p}(实例独占物落在内容根)") + assert not offenders, "内容根被实例化:\n - " + "\n - ".join(offenders) + print(f" ✓ 内容根干净({len(list(STORE.iterdir()))} 个版本目录)") + + +def test_test_instances_live_in_instances_layer(): + """测试用的实例目录必须落在 instances/ 层,且不在任何内容根下。""" + missing = [] + for name in TEST_INSTANCES: + p = INSTANCES / name + if not p.exists(): + missing.append(name) + continue + assert STORE not in p.parents, f"实例落在了内容根里:{p}" + # 缺目录不算失败(测试会自建),但已存在的必须位置正确 + print(f" ✓ 测试实例归位({len(TEST_INSTANCES) - len(missing)}/{len(TEST_INSTANCES)} 已建)") + + +def test_content_root_has_no_shared_instance_config(): + """内容根里的 options.txt / mods 是「共用」的直接证据,必须为空。 + + ★ 与上一条的区别:上一条查「存在」,这条查「内容」—— + 空目录也可能只是没写进去,所以要看是否真有文件。 + """ + if not STORE.is_dir(): + print(" - 跳过(无 store/)") + return + bad = [] + for root in sorted(p for p in STORE.iterdir() if p.is_dir()): + for name in ("mods", "config"): + d = root / name + if d.is_dir() and any(d.iterdir()): + bad.append(f"{d}({len(list(d.iterdir()))} 项)") + assert not bad, "共享内容根里有实例配置(会让多实例互相污染):\n - " + "\n - ".join(bad) + print(" ✓ 内容根无实例配置") + + +if __name__ == "__main__": + tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] + failed = [] + for t in tests: + try: + t() + except AssertionError as e: + failed.append(f"{t.__name__}: {e}") + print(f" ✗ {t.__name__}: {e}") + except Exception as e: + failed.append(f"{t.__name__}: {e!r}") + print(f" ✗ {t.__name__}: {e!r}") + print() + if failed: + print(f"版本隔离守卫: {len(tests)-len(failed)}/{len(tests)} 通过") + sys.exit(1) + print(f"版本隔离守卫: {len(tests)}/{len(tests)} 全部通过")