Files
agentmc/tests/test_server.py
JianFeeeee 480fe8e225 M0 完成:空白测试服(vanilla 1.21.1 / 0 mods)
## 为什么不用现役服(用户定向)

> 我觉得可以起一个空白新服用来测试功能,而不是用现在的服。

理由不只是"干净",而是**可预期**:
现役 `/home/minecraft` 是 world 15G + 64 mods + neoforge ——
任何协议层改动都可能污染它,且**测不出边界**(模组太多、状态太杂)。
我们要验的是**协议与行为**:已知方块集、已知出生点、可重置的世界。

## 形态

    store/vanilla-1.21.1/server/    内容根  jar + 配置 + world
    runtime/test-server/            运行期  server.log / server.pid

- **vanilla 1.21.1**,0 mods(协议最干净,先验协议再逐个加 mod 验差异)
- **超平坦 + 固定种子** `mcfold-test-2026`(地形可预测)
- creative / peaceful / 无怪物 / 无结构 / spawn-protection=0
- **端口 25566**(避开现役 25565)
- server.jar 下载后**校验 sha1**(`59353fb...`,实测一致)

## mcfold/server.py:起停 + 探活

`python3 -m mcfold.server {start,stop,status,reset}`

★ **status 是协议级探活**(Server List Ping,state=1),不是"端口开着就算活":
必须回出 version / protocol / motd 才算真起来。
实测:`1.21.1 protocol 767  players 0/4  motd=mcfold test server`

## tests/test_server.py(4 项,默认套件)

  ✓ 配置生成一致  —— ★ 守护一个真实踩过的坑:
      properties 里写**行内注释**(`k=v  # 注释`)会被解析成值的一部分。
      我第一版 `level-type=minecraft\:flat  \# 超平坦` 就是这样坏掉的
      (服务端把它当成值,且 spawn-protection 也回到默认)。
      ⇒ 生成器**不写行内注释**,这条判据守它。
  ✓ 三层隔离(内容根 vs 运行期;日志不落内容根)
  ✓ 端口避让(25566 ≠ 现役 25565)
  ✓ **协议级探活**(起服时验 protocol == 767;没起服则跳过,不误报)

## 变异验证(工程纪律 #2:判据必须能失败)

把 `PROTOCOL = 767` 改成 `999` ⇒ `test_status_proves_protocol` **变红**
("协议号不符:767 != 999"),还原后恢复绿。
⇒ 这条判据不是恒绿。

## 测试

  ./run_tests.sh  → **70 项**(原 66 + 本套件 4),0 失败
2026-09-26 11:12:40 +08:00

91 lines
3.8 KiB
Python
Raw Permalink 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.

"""空白测试服(M0)的判据。
★ 最重要的一条:判据必须能**失败**(工程纪律 #2)。
本文件的 test_status_proves_protocol 靠**真的连一次服务端**来判 ——
端口开着但服务端不是 MC,它会红。
没起服时**跳过**(不误报失败)。
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from mcfold import server # noqa: E402
def test_config_is_generated_consistently():
"""配置文件由模块生成 ⇒ 内容必须与常量一致(防手工改歪)。
★ 踩过的坑:properties 里写行内注释(`k=v # 注释`)会被解析成
值的一部分 —— `level-type=minecraft\\:flat \\# 超平坦` 就是这样坏的。
所以生成器**不写行内注释**,这条守护它。
"""
gen = server.SERVER_PROPERTIES.format(port=server.PORT)
for line in gen.splitlines():
if line.startswith("#") or not line.strip():
continue
assert " #" not in line, f"不能有行内注释(会被当值):{line}"
assert "#" not in line.split("=", 1)[1], f"值里不能有 #:{line}"
assert f"server-port={server.PORT}" in gen
assert "level-type=minecraft\\:flat" in gen
assert "online-mode=false" in gen
print(f" ✓ 配置生成一致(port={server.PORT}, flat, offline)")
def test_three_layer_isolation():
"""内容根与运行期必须是**不同**的层(版本隔离开支)。"""
assert server.CONTENT != server.RUNTIME
assert "store" in str(server.CONTENT), f"服务端内容应在 store/ 下:{server.CONTENT}"
assert "runtime" in str(server.RUNTIME), f"日志应在 runtime/ 下:{server.RUNTIME}"
# 日志不能写进内容根(那是可只读共享的)
assert server.CONTENT not in server.LOG.parents, \
f"日志不该落在内容根:{server.LOG}"
print(f" ✓ 三层隔离(内容={server.CONTENT.name}/ 运行期={server.RUNTIME.name}/)")
def test_port_avoids_production():
"""端口必须避开现役服的 25565。"""
assert server.PORT != 25565, "测试服不该占现役端口"
print(f" ✓ 端口避让(测试 {server.PORT} vs 现役 25565)")
def test_status_proves_protocol():
"""★ 核心判据:服务端的 status 查询必须回出 MC 协议信息。
这一条**能失败**:
· 端口没开 ⇒ status() 返回 None ⇒ 跳过(不是失败)
· 端口开着但非 MC ⇒ status() 返回 None ⇒ 跳过(无法区分,诚实跳过)
· 服务端真在跑 ⇒ 必须回出 protocol == 767
起服时它验协议;没起服时它不误报。
"""
st = server.status()
if st is None:
print(f" - 跳过({server.PORT} 上没探到 MC 服务端)")
return
assert st["version"] == server.MC_VERSION, \
f"版本不符:{st['version']} != {server.MC_VERSION}"
assert st["protocol"] == server.PROTOCOL, \
f"协议号不符:{st['protocol']} != {server.PROTOCOL}"
assert st["players_max"] > 0, st
print(f" ✓ 协议级探活({st['version']} protocol {st['protocol']},"
f"players {st['players_online']}/{st['players_max']})")
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)} 全部通过")