## 为什么不用现役服(用户定向)
> 我觉得可以起一个空白新服用来测试功能,而不是用现在的服。
理由不只是"干净",而是**可预期**:
现役 `/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 失败
91 lines
3.8 KiB
Python
91 lines
3.8 KiB
Python
"""空白测试服(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)} 全部通过")
|