Files
cultivation-world-simulator/tests/test_history.py
2026-01-18 16:53:24 +08:00

245 lines
9.7 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.
import pytest
import json
from unittest.mock import MagicMock, AsyncMock, patch
from pathlib import Path
from src.classes.history import HistoryManager, History
from src.classes.region import CityRegion, NormalRegion, CultivateRegion
from src.classes.sect_region import SectRegion
from src.classes.technique import Technique, TechniqueAttribute, TechniqueGrade
from src.classes.weapon import Weapon, WeaponType
from src.classes.auxiliary import Auxiliary
from src.classes.cultivation import Realm
from src.classes.item_registry import ItemRegistry
from src.classes.sect import Sect, SectHeadQuarter
from src.classes.alignment import Alignment
from src.sim.load.load_game import apply_history_modifications
# 假设这些全局字典在模块层级
from src.classes import technique as technique_module
from src.classes import weapon as weapon_module
from src.classes import sect as sect_module
from src.classes import auxiliary as auxiliary_module
# --- 1. 基础数据结构测试 (Plan 1) ---
def test_world_history_structure(base_world):
"""
目标:验证 World 与 History dataclass 的交互是否符合预期。
"""
# 初始化:验证 world.history 是否自动初始化为空的 History 对象
assert isinstance(base_world.history, History)
assert base_world.history.text == ""
assert base_world.history.modifications == {}
# 设置文本:验证 text 更新
history_text = "修仙界风云变幻"
base_world.set_history(history_text)
assert base_world.history.text == history_text
# 记录差分:验证 record_modification
# 第一次记录
base_world.record_modification("sects", "1", {"name": "新名字"})
assert "sects" in base_world.history.modifications
assert "1" in base_world.history.modifications["sects"]
assert base_world.history.modifications["sects"]["1"]["name"] == "新名字"
# 第二次记录:验证合并 (Merge)
base_world.record_modification("sects", "1", {"desc": "新描述"})
assert base_world.history.modifications["sects"]["1"]["name"] == "新名字" # 旧属性保留
assert base_world.history.modifications["sects"]["1"]["desc"] == "新描述" # 新属性添加
# 第三次记录:验证覆盖 (Override)
base_world.record_modification("sects", "1", {"name": "更新的名字"})
assert base_world.history.modifications["sects"]["1"]["name"] == "更新的名字"
# --- 2. 修改记录行为测试 (Plan 2) ---
def test_history_manager_records_changes(base_world):
"""
目标:验证 HistoryManager 在修改对象时,是否自动产生“留痕”。
"""
# Setup
sect = Sect(id=1, name="OldSect", desc="OldDesc", member_act_style="", alignment=Alignment.RIGHTEOUS, headquarter=None, technique_names=[])
manager = HistoryManager(base_world)
# Execute Modification
# 模拟 _update_obj_attrs 的调用
changes = {"name": "NewSect", "desc": "NewDesc"}
manager._update_obj_attrs(sect, changes, category="sects", id_str="1")
# Verify Object Updated
assert sect.name == "NewSect"
assert sect.desc == "NewDesc"
# Verify History Recorded
assert "sects" in base_world.history.modifications
assert "1" in base_world.history.modifications["sects"]
recorded_change = base_world.history.modifications["sects"]["1"]
assert recorded_change["name"] == "NewSect"
assert recorded_change["desc"] == "NewDesc"
# --- 3. 差分回放逻辑测试 (Plan 3) ---
def test_apply_history_modifications_logic(base_world):
"""
目标:验证 apply_history_modifications 函数能否将数据字典正确应用到静态对象上。
"""
# Setup Objects
sect = Sect(id=1, name="OriginalSect", desc="OriginalDesc", member_act_style="", alignment=Alignment.RIGHTEOUS, headquarter=None, technique_names=[])
weapon = Weapon(id=101, name="OriginalSword", weapon_type=WeaponType.SWORD, realm=Realm.Qi_Refinement, desc="OriginalDesc")
# Patch Global Registries
with patch.dict(sect_module.sects_by_id, {1: sect}, clear=True), \
patch.dict(sect_module.sects_by_name, {"OriginalSect": sect}, clear=True), \
patch.object(ItemRegistry, "get", return_value=weapon), \
patch.dict(weapon_module.weapons_by_name, {"OriginalSword": weapon}, clear=True):
# Construct Modifications
modifications = {
"sects": {
"1": {"name": "ReplayedSect", "desc": "ReplayedDesc"},
"999": {"name": "GhostSect"} # 不存在的 ID应忽略
},
"weapons": {
"101": {"name": "ReplayedSword"}
}
}
# Execute Replay
apply_history_modifications(base_world, modifications)
# Verify Sect Updated
assert sect.name == "ReplayedSect"
assert sect.desc == "ReplayedDesc"
# Verify Sect Index Synced (Old name removed, new name added)
assert "OriginalSect" not in sect_module.sects_by_name
assert "ReplayedSect" in sect_module.sects_by_name
assert sect_module.sects_by_name["ReplayedSect"] == sect
# Verify Weapon Updated
assert weapon.name == "ReplayedSword"
assert weapon.desc == "OriginalDesc" # Unchanged
# Verify Weapon Index Synced
assert "OriginalSword" not in weapon_module.weapons_by_name
assert "ReplayedSword" in weapon_module.weapons_by_name
# --- 4. 集成测试:存读档全流程 (Plan 4) ---
def test_save_load_integration_with_history(base_world, tmp_path):
"""
目标:模拟真实游戏场景,验证“修改 -> 存档 -> 重置 -> 读档 -> 还原”的闭环。
"""
from src.sim.save.save_game import save_game
from src.sim.load.load_game import load_game
from src.sim.simulator import Simulator
# 1. Setup Initial State
sect = Sect(id=1, name="OriginalSect", desc="OriginalDesc", member_act_style="", alignment=Alignment.RIGHTEOUS, headquarter=None, technique_names=[])
# Patch 全局状态,模拟游戏运行环境
with patch.dict(sect_module.sects_by_id, {1: sect}, clear=True), \
patch.dict(sect_module.sects_by_name, {"OriginalSect": sect}, clear=True):
# 2. Apply Changes & Record History
history_text = "History Text"
base_world.set_history(history_text)
# 模拟 HistoryManager 的修改操作
sect.name = "ModifiedSect"
base_world.record_modification("sects", "1", {"name": "ModifiedSect"})
# 此时内存中是 ModifiedSect
simulator = Simulator(base_world)
existed_sects = [sect]
# 3. Save Game
save_path = tmp_path / "integration_save.json"
save_game(base_world, simulator, existed_sects, save_path)
# 4. Reset Memory (Simulate Restart)
# 将对象重置为原始状态,模拟重新加载配置文件的过程
sect.name = "OriginalSect"
if "ModifiedSect" in sect_module.sects_by_name:
del sect_module.sects_by_name["ModifiedSect"]
sect_module.sects_by_name["OriginalSect"] = sect
assert sect.name == "OriginalSect" # 确认重置成功
# 5. Load Game
# load_game 会调用 apply_history_modifications
# 注意load_game 内部会导入 sects_by_id我们已经在 patch 中了,所以 load_game 会看到我们 patch 的 dict
# 但 load_game 会重建 world所以我们需要验证 loaded_world
loaded_world, _, _ = load_game(save_path)
# 6. Verify
# 验证历史文本
assert loaded_world.history.text == history_text
# 验证 Modifications 数据存在
assert "sects" in loaded_world.history.modifications
assert loaded_world.history.modifications["sects"]["1"]["name"] == "ModifiedSect"
# 核心验证:内存中的对象是否变回了 ModifiedSect
# 因为我们 patch 了全局字典load_game 回放时修改的就是这个全局字典里的 sect 对象
assert sect.name == "ModifiedSect"
assert "ModifiedSect" in sect_module.sects_by_name
assert "OriginalSect" not in sect_module.sects_by_name
# --- 5. 边界情况测试 (Plan 5) ---
def test_history_boundary_cases(base_world, tmp_path):
"""
目标:边界情况测试
"""
from src.sim.save.save_game import save_game
from src.sim.load.load_game import load_game
from src.sim.simulator import Simulator
# Case 1: Empty History
# 保存一个没有历史修改的存档
simulator = Simulator(base_world)
save_path = tmp_path / "empty_history.json"
save_game(base_world, simulator, [], save_path)
# 读档应不报错
loaded_world, _, _ = load_game(save_path)
assert loaded_world.history.text == ""
assert loaded_world.history.modifications == {}
# Case 2: Corrupted/Partial Modification Data (Manual JSON edit)
# 创建一个手动修改的 JSON模拟数据损坏或旧版本残留
with open(save_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 注入一个格式奇怪的 modifications
data["world"]["history"] = {
"text": "Partial",
"modifications": {
"sects": {
"invalid_id": {"name": "ShouldNotCrash"} # ID 不是数字,但 key 是 str
},
"unknown_category": { # 未知类别
"1": {"name": "???"}
}
}
}
with open(save_path, "w", encoding="utf-8") as f:
json.dump(data, f)
# 读档应鲁棒处理,不崩溃
loaded_world_2, _, _ = load_game(save_path)
assert loaded_world_2.history.text == "Partial"
# 确保未知类别被加载(作为数据),但在 apply 时被忽略(不报错)
assert "unknown_category" in loaded_world_2.history.modifications