Files
agentmc/tests/test_servers.py
JianFeeeee 554ebc239a M10c:mod 环境下寻路 —— 问运行时而非猜名字(+ 语义表与覆盖率报告)
用户要求:Create 与 Aeronautics 会引入大量原版没有的方块与危险区,
寻路必须一起优化。用户裁定:危险区用「表 + 命名推测并标记」,
动态机械用「警告 + 提高代价」。

## ★ 关键实测结论(本轮最重要)

我先按「给 mod 方块建语义表」做,实测覆盖率:

    Create 643 个方块 → 显式表命中 9、命名规则推测 21、完全不识 613
    覆盖率 4.7%

⇒ **手写表是死路**(64 mod / 1465 方块永远追不上)。
⇒ 但寻路真正需要的属性,**运行时全都有**:
   Java 侧 blockinfo 已在读 BuiltInRegistries.BLOCK(**含 mod 运行时注册的方块**),
   并返回 isSolid / destroySpeed / lightEmission / isAir / hasBlockEntity。
⇒ 正确做法是**问**,不是猜。

## 改动

新增 mcfold/solid.py:
- SolidSource 抽象:RegistrySolidSource(本机,快但无 mod)
  vs RuntimeSolidSource(问 JVM,**含 mod**,准但要 JVM)
- 按 **state_id** 缓存(属性只依赖 state,不依赖坐标)
- attach_runtime() 批量探测(一次问一格要一次往返,批量才可用)

mcfold/pathfind.py:Terrain 改为「**先运行时缓存,再本机注册表**」;
  can_dig 用 destroySpeed 判可挖(基岩官方约定是 -1 = 不可破坏),
  不再靠名字猜「基岩挖不动」

新增 mcfold/modblocks.py(危险/会动这部分的表,配套覆盖率报告):
- 语义取���:ground/passable/hazard/breakable/solid/dynamic/carrier/unknown
- 三级来源:显式表 → 命名规则(**标记 inferred**)→ **如实说不认识**
- ★ vanilla 与 mod **分开算覆盖率**:vanilla 语义由注册表给,
  把它算成「未知」会让人误以为「连石头都认不出」

WorldState:加 solid_cache 字段

## 判据

test_pathfind 19 → 24 项,新增 5 项:
- mod 方块优先用运行时 isSolid(本机注册表确实认不出)
- 不可破坏吃 destroySpeed(含基岩 -1 约定)
- 两边都无数据 ⇒ 不知道(**不谎称可走**)
- 覆盖率报告如实(且区分 mod/vanilla)
- 语义查询永远返回带 inferred 标记的结构

★ 写判据时踩了两个坑(都是 fixture 形状错,不是代码错):
  1. `Chunk.sections[i]` 对应 y = MIN_Y + 16*i,MIN_Y=-64
     ⇒ 我按「第 0 个 section = y=0」写,查 y=0 拿到的是空气
  2. 同一 section 内 16 格**只能一个值** ⇒ mod 方块与空气落同一 section 时,
     空气格会变成 mod 方块(看着像代码 bug,其实是 fixture 形状问题)
  ⇒ 教训:**判据的 fixture 形状必须与被测代码的真实输入结构一致**。

226 项全通过。
2026-09-26 14:29:58 +08:00

249 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""M10b 加载器服务端判据:servers.py。
## 判据要点
1. **隔离**:mods 落**实例层**,内容根**零写入**
(这是 `test_version_isolation` 守的同一条不变量,加载器不能破坏它)
2. **未接的加载器要明说**,不能瞎猜或静默降级成 vanilla
3. **缺文件要指名道姓**,不能吞掉
4. 全程用**临时目录**,不碰真实 mcview
## 不测什么
下载安装器、起 JVM —— 那些需要网络/环境,放真实使用时验。
"""
import sys, os, json, tempfile, stat
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import mcfold.servers as S
def _tmp():
"""把模块的路径常量指到临时目录(隔离真实 mcview)。"""
tmp = Path(tempfile.mkdtemp(prefix="mcfold-srv-test-"))
S.BASE = tmp
S.STORE = tmp / "store"
S.INSTANCES = tmp / "instances"
S.RUNTIME = tmp / "runtime"
S.LOADER_CACHE = tmp / "loaders"
return tmp
def _fake_jar(tmp: Path, name: str = "server.jar", size: int = 5000) -> Path:
p = tmp / name
p.write_bytes(b"PK\x03\x04" + b"0" * size)
return p
# ---------- ① 隔离:mods 在实例层,内容根零写入 ----------
def test_mods_land_in_instance_not_content_root():
"""★★ mods 必须落**实例层**;内容根保持只读共享。
为什么这条是硬要求:一个 vanilla 内容根旁边要能起多个「装了不同 mod 的服」。
mods 若写进内容根 ⇒ 那些服会互相看到对方的 mod(这正是版本隔离要防的崩因)。
"""
tmp = _tmp()
jar = _fake_jar(tmp)
mod = _fake_jar(tmp, "coolmod.jar")
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
mod=[str(mod)], server_jar=str(jar))
assert r["ok"] is True, r
inst = Path(r["instance"])
assert (inst / "mods" / "coolmod.jar").exists(), "mod 应在实例的 mods/ 里"
# ★ 内容根零写入
assert not S.STORE.exists() or not any(S.STORE.iterdir()), \
f"内容根被写入了:{list(S.STORE.iterdir()) if S.STORE.exists() else []}"
print(" ★★ mods 落实例层,内容根零写入")
def test_two_instances_share_content_root_different_mods():
"""★ 同一版本的两个实例可装**不同** mod(多实例的意义)。"""
tmp = _tmp()
jar = _fake_jar(tmp)
a = _fake_jar(tmp, "modA.jar")
b = _fake_jar(tmp, "modB.jar")
r1 = S.create_modded_server("instA", version="1.21.1", loader="vanilla",
mod=[str(a)], server_jar=str(jar))
r2 = S.create_modded_server("instB", version="1.21.1", loader="vanilla",
mod=[str(b)], server_jar=str(jar))
assert r1["ok"] and r2["ok"]
ma = sorted(p.name for p in (Path(r1["instance"]) / "mods").iterdir())
mb = sorted(p.name for p in (Path(r2["instance"]) / "mods").iterdir())
assert ma == ["modA.jar"] and mb == ["modB.jar"], (ma, mb)
print(" ★ 同版本两实例各装各的 mod(互不看见)")
def test_instance_json_records_version_and_loader(monkey=None):
"""★ 实例元数据要记 version/loader —— 隔离的**依据**。
(之前 7 个实例里有 2 个缺 instance.json,list_instances 只能报
has_meta: False ⇒ 隔离没有可核查的依据。)
★ fabric 路径会去**下载** server-launch ⇒ 判据里替换掉下载函数,
避免判据依赖网络(网络一挂就假红)。
"""
tmp = _tmp()
jar = _fake_jar(tmp)
# ★ fabric 现在走 classpath 启动(实测 maven 上没有 server-launch 产物)
# ⇒ 判据替换「库解析」,不依赖网络与本地资产
libs = tmp / "fakelibs"
libs.mkdir()
for n in ("fabric-loader.jar", "intermediary.jar"):
(libs / n).write_bytes(b"PK\x03\x04" + b"0" * 5000)
orig = S.fabric_libraries
S.fabric_libraries = lambda v, loader_version="": {
"ok": True, "jars": [str(p) for p in sorted(libs.glob("*.jar"))],
"source": str(libs), "cached": True, "_stub": True}
try:
r = S.create_modded_server("inst1", version="1.20.1", loader="fabric")
finally:
S.fabric_libraries = orig
assert r["ok"] is True, r
meta = json.loads((Path(r["instance"]) / "instance.json").read_text())
assert meta["version"] == "1.20.1", meta
assert meta["loader"] == "fabric", meta
assert meta["role"] == "server", meta
print(" ★ instance.json 记全 version/loader/role")
def test_runtime_dir_is_third_layer():
"""★ 运行时目录(world/logs/pid)在第三层,不在实例或内容根里。"""
tmp = _tmp()
jar = _fake_jar(tmp)
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
server_jar=str(jar))
rt = Path(r["runtime"])
assert rt.parent == S.RUNTIME, rt
assert rt.name == "inst1", rt
assert "/store/" not in str(rt) and "/instances/" not in str(rt)
print(f" ★ 运行时目录在第三层({rt})")
# ---------- ② 未接的加载器必须明说 ----------
def test_unimplemented_loader_says_so():
"""★★ 没接的加载器(Forge/Quilt)必须**明说**,不能静默降级。
静默降级成 vanilla 的后果很严重:服起来了但**mod 全部没加载**,
而使用者以为装上了 ⇒ 后面所有观察都基于错误前提。
"""
tmp = _tmp()
jar = _fake_jar(tmp)
for loader in ("forge", "quilt"):
r = S.create_modded_server(f"inst-{loader}", version="1.21.1",
loader=loader, server_jar=str(jar))
assert r["ok"] is False, f"{loader} 不该成功"
assert loader in r["why"], r
assert r.get("how_to"), r
# ★ 不能建出实例目录(否则会留个「看着存在但没配好」的壳)
assert not (S.INSTANCES / f"inst-{loader}").exists(), \
f"{loader} 失败时不该留下实例目录"
print(" ★★ NeoForge/Forge 明确说未接(不静默降级成 vanilla)")
# ---------- ③ 缺文件要指名道姓 ----------
def test_missing_mod_files_are_named():
"""★ 缺的 mod 要**逐个列出来**(不能吞掉让人以为全装上了)。"""
tmp = _tmp()
jar = _fake_jar(tmp)
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
mod=["/no/such1.jar", "/no/such2.jar"],
server_jar=str(jar))
assert r["ok"] is False, r
assert "such1" in r["why"] and "such2" in r["why"], r["why"]
assert r.get("installed") == [], r.get("installed")
print(" ★ 缺 mod 逐个指名(不静默吞)")
def test_neoforge_requires_install_flag():
"""★ NeoForge 没给 install=True 要**明说**(不能假装装好了)。
NeoForge 服务端必须由官方 installer(--installServer)生成,
没有它就只有一堆 mod jar 而没有可运行的服。
"""
tmp = _tmp()
jar = _fake_jar(tmp)
r = S.create_modded_server("inst-nf", version="1.21.1", loader="neoforge",
mod=[str(jar)], install=False)
assert r["ok"] is False, r
assert "installer" in r["why"], r
assert r.get("how_to"), r
assert not (S.INSTANCES / "inst-nf").exists(), "失败不该留实例目录"
print(" ★ NeoForge 未 install:明说要跑 installer")
def test_missing_server_jar_for_vanilla_is_reported():
"""★ vanilla 没给 server.jar 要明说(不能起个残缺服)。"""
tmp = _tmp()
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla")
assert r["ok"] is False, r
assert "server_jar" in r["why"], r
assert r.get("how_to"), r
print(" ★ vanilla 缺 server.jar 明说")
def test_bad_instance_name_rejected():
"""★ 实例名要校验(它进路径,含分隔符会越界)。"""
tmp = _tmp()
for bad in ("../escape", "a/b", "", "x" * 100, "has space"):
r = S.create_modded_server(bad, version="1.21.1", loader="vanilla",
server_jar=str(_fake_jar(tmp)))
assert r["ok"] is False, f"{bad!r} 应被拒"
print(" ★ 非法实例名被拒(含路径分隔符)")
# ---------- ④ 启动脚本 ----------
def test_start_script_is_executable_and_self_contained():
"""★ 脚本要可执行且**自带 cd**(内容根零写入 ⇒ 不能依赖调用者 cwd)。"""
tmp = _tmp()
jar = _fake_jar(tmp)
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
server_jar=str(jar))
script = Path(r["start_script"])
assert script.exists(), script
assert script.stat().st_mode & stat.S_IXUSR, "应可执行"
body = script.read_text(encoding="utf-8")
assert 'cd "$(dirname "$0")"' in body, "应自带 cd 到实例目录"
assert "server.jar" in body
print(" ★ 启动脚本可执行且自带 cd")
def test_repeated_creation_is_idempotent_report():
"""★ 重复创建同一实例要**如实报告**(不假装全新,也不默默覆盖 mod)。"""
tmp = _tmp()
jar = _fake_jar(tmp)
a = _fake_jar(tmp, "modA.jar")
r1 = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
mod=[str(a)], server_jar=str(jar))
r2 = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
mod=[str(a)], server_jar=str(jar))
assert r1["ok"] and r2["ok"]
# 两次都成功(幂等),且 mod 没被重复堆积
mods = sorted(p.name for p in (Path(r2["instance"]) / "mods").iterdir())
assert mods == ["modA.jar"], mods
print(" ★ 重复创建幂等(mod 不堆积)")
if __name__ == "__main__":
for fn in [test_mods_land_in_instance_not_content_root,
test_two_instances_share_content_root_different_mods,
test_instance_json_records_version_and_loader,
test_runtime_dir_is_third_layer,
test_unimplemented_loader_says_so,
test_missing_mod_files_are_named,
test_neoforge_requires_install_flag,
test_missing_server_jar_for_vanilla_is_reported,
test_bad_instance_name_rejected,
test_start_script_is_executable_and_self_contained,
test_repeated_creation_is_idempotent_report]:
fn()
print("\n加载器服务端:全部通过")