- Added `owned_regions` attribute to the Avatar class to track regions owned by avatars. - Introduced `occupy_region` and `release_region` methods for managing region ownership and ensuring proper relationship handling. - Updated AvatarManager to clear relationships when an avatar is released, ensuring no lingering references. - Refactored region ownership logic in the Occupy action and Simulator to utilize the new methods for better clarity and maintainability. - Enhanced game loading process to establish ownership relationships correctly during game state restoration.
241 lines
8.8 KiB
Python
241 lines
8.8 KiB
Python
"""
|
||
测试 CSV 数据加载的正确性。
|
||
验证代码中使用的列名与 CSV 文件中的实际列名匹配。
|
||
采用动态多语言测试方案,不再硬编码特定语言的预期字符串。
|
||
"""
|
||
import pytest
|
||
import csv
|
||
from pathlib import Path
|
||
from src.classes.sect import sects_by_id, sects_by_name, Sect, reload as reload_sects
|
||
from src.classes.technique import techniques_by_id, techniques_by_name, Technique, reload as reload_techniques
|
||
from src.classes.elixir import elixirs_by_id
|
||
from src.utils.config import CONFIG
|
||
from src.i18n import t, reload_translations
|
||
from src.classes.language import language_manager
|
||
|
||
# --- Helpers ---
|
||
|
||
def read_raw_csv_as_dict(file_path):
|
||
"""读取原始 CSV 文件,跳过描述行"""
|
||
if not file_path.exists():
|
||
return []
|
||
|
||
with open(file_path, 'r', encoding='utf-8-sig') as f:
|
||
lines = list(csv.reader(f))
|
||
|
||
if len(lines) < 1:
|
||
return []
|
||
|
||
headers = lines[0]
|
||
data = []
|
||
|
||
# Start from index 2 if there's a description row
|
||
start_index = 2 if len(lines) > 1 else 1
|
||
|
||
for row_values in lines[start_index:]:
|
||
if not row_values: continue
|
||
row_dict = {}
|
||
for i, h in enumerate(headers):
|
||
if i < len(row_values):
|
||
row_dict[h] = row_values[i]
|
||
else:
|
||
row_dict[h] = None
|
||
data.append(row_dict)
|
||
|
||
return data
|
||
|
||
@pytest.fixture(params=["zh-CN", "zh-TW", "en-US"])
|
||
def game_lang(request):
|
||
"""
|
||
参数化 Fixture:切换语言并重载游戏数据。
|
||
测试结束后自动恢复回 zh-CN 环境。
|
||
"""
|
||
lang = request.param
|
||
|
||
# 1. Switch Language
|
||
language_manager.set_language(lang)
|
||
reload_translations()
|
||
|
||
# 2. Force Reload Game Data
|
||
from src.utils.config import update_paths_for_language
|
||
update_paths_for_language(lang)
|
||
|
||
from src.utils.df import reload_game_configs
|
||
reload_game_configs()
|
||
|
||
reload_techniques()
|
||
reload_sects()
|
||
|
||
yield lang
|
||
|
||
# Teardown: Restore to zh-CN for other tests
|
||
language_manager.set_language("zh-CN")
|
||
reload_translations()
|
||
update_paths_for_language("zh-CN")
|
||
reload_game_configs()
|
||
reload_techniques()
|
||
reload_sects()
|
||
|
||
|
||
class TestSectLoading:
|
||
"""测试宗门数据加载 (多语言动态验证)"""
|
||
|
||
def test_sect_headquarter_name_loaded(self, game_lang):
|
||
"""测试宗门驻地名称正确加载"""
|
||
# Read RAW Sect CSV
|
||
sect_csv_path = CONFIG.paths.shared_game_configs / "sect.csv"
|
||
raw_sects = read_raw_csv_as_dict(sect_csv_path)
|
||
|
||
# Read RAW Sect Region CSV (Source of HQ names)
|
||
region_csv_path = CONFIG.paths.shared_game_configs / "sect_region.csv"
|
||
raw_regions = read_raw_csv_as_dict(region_csv_path)
|
||
sect_region_map = {int(r['sect_id']): r for r in raw_regions if r.get('sect_id')}
|
||
|
||
# Verify specific Sect (ID=12, 不夜城)
|
||
target_id = 12
|
||
sect = sects_by_id.get(target_id)
|
||
assert sect is not None
|
||
|
||
# 1. Verify Sect Name
|
||
sect_row = next((r for r in raw_sects if int(r['id']) == target_id), None)
|
||
assert sect_row
|
||
|
||
expected_sect_name = sect_row.get('name')
|
||
if sect_row.get('name_id'):
|
||
trans = t(sect_row['name_id'])
|
||
if trans and trans != sect_row['name_id']:
|
||
expected_sect_name = trans
|
||
|
||
assert sect.name == expected_sect_name, f"Sect name mismatch in {game_lang}"
|
||
|
||
# 2. Verify HQ Name
|
||
region_row = sect_region_map.get(target_id)
|
||
assert region_row
|
||
|
||
expected_hq_name = region_row.get('name')
|
||
if region_row.get('name_id'):
|
||
trans = t(region_row['name_id'])
|
||
if trans and trans != region_row['name_id']:
|
||
expected_hq_name = trans
|
||
|
||
assert sect.headquarter.name == expected_hq_name, f"HQ name mismatch in {game_lang}"
|
||
|
||
def test_sect_headquarter_desc_loaded(self, game_lang):
|
||
"""测试宗门驻地描述正确加载"""
|
||
target_id = 12
|
||
sect = sects_by_id.get(target_id)
|
||
assert sect is not None
|
||
|
||
# Read RAW Sect Region CSV
|
||
region_csv_path = CONFIG.paths.shared_game_configs / "sect_region.csv"
|
||
raw_regions = read_raw_csv_as_dict(region_csv_path)
|
||
region_row = next((r for r in raw_regions if int(r.get('sect_id', -1)) == target_id), None)
|
||
assert region_row
|
||
|
||
expected_desc = region_row.get('desc')
|
||
if region_row.get('desc_id'):
|
||
trans = t(region_row['desc_id'])
|
||
if trans and trans != region_row['desc_id']:
|
||
expected_desc = trans
|
||
|
||
# Normalize newlines/spaces for comparison if needed
|
||
assert sect.headquarter.desc == expected_desc, f"HQ desc mismatch in {game_lang}"
|
||
|
||
def test_all_sects_have_headquarters(self, game_lang):
|
||
"""测试所有宗门都有驻地信息"""
|
||
for sect_id, sect in sects_by_id.items():
|
||
assert sect.headquarter is not None
|
||
assert sect.headquarter.name, f"宗门 {sect.name} 的驻地名称不应为空"
|
||
|
||
def test_sect_techniques_loaded(self, game_lang):
|
||
"""测试宗门功法列表正确加载"""
|
||
sect = sects_by_id.get(1) # 明心剑宗
|
||
assert sect is not None
|
||
assert len(sect.technique_names) > 0
|
||
|
||
def test_sect_without_techniques(self, game_lang):
|
||
"""测试没有配置功法的宗门"""
|
||
sect = sects_by_id.get(12) # 不夜城
|
||
assert sect is not None
|
||
assert sect.technique_names == []
|
||
|
||
|
||
class TestTechniqueLoading:
|
||
"""测试功法数据加载"""
|
||
|
||
def test_technique_sect_id_loaded(self, game_lang):
|
||
"""测试功法的 sect_id 正确加载"""
|
||
tech_id = 30 # 草字剑诀
|
||
technique = techniques_by_id.get(tech_id)
|
||
assert technique is not None
|
||
|
||
# Verify Name using Dynamic Logic
|
||
tech_csv_path = CONFIG.paths.shared_game_configs / "technique.csv"
|
||
raw_techs = read_raw_csv_as_dict(tech_csv_path)
|
||
row = next((r for r in raw_techs if int(r['id']) == tech_id), None)
|
||
|
||
expected_name = row.get('name')
|
||
if row.get('name_id'):
|
||
trans = t(row['name_id'])
|
||
if trans and trans != row['name_id']:
|
||
expected_name = trans
|
||
|
||
assert technique.name == expected_name, f"Technique name mismatch in {game_lang}"
|
||
assert technique.sect_id == 1
|
||
|
||
def test_technique_without_sect(self, game_lang):
|
||
"""测试散修功法"""
|
||
technique = techniques_by_id.get(1)
|
||
assert technique is not None
|
||
assert technique.sect_id is None
|
||
|
||
def test_sect_techniques_match(self, game_lang):
|
||
"""测试宗门功法和功法的宗门ID相互匹配"""
|
||
for sect_id, sect in sects_by_id.items():
|
||
for tech_name in sect.technique_names:
|
||
technique = techniques_by_name.get(tech_name)
|
||
# 注意:technique_names 是 string list,如果 names 不匹配(翻译问题)这里会取不到
|
||
# 但我们的系统设计是:sect.technique_names 是直接从 technique.csv 加载的
|
||
# 所以只要 reload 顺序正确(先 technique 后 sect),名字应该是一致的
|
||
assert technique is not None, f"功法 '{tech_name}' 应该存在 (Lang: {game_lang})"
|
||
assert technique.sect_id == sect_id
|
||
|
||
|
||
class TestElixirLoading:
|
||
"""丹药加载测试 (ID check, less dependent on lang but good to verify integrity)"""
|
||
|
||
def test_elixir_loaded_with_item_id(self):
|
||
# 丹药目前没有专门的 reload 和 translation key 绑定逻辑验证需求
|
||
# 保持原样即可,不需要 parametrizing unless needed
|
||
assert len(elixirs_by_id) > 0
|
||
for elixir_id, elixir in elixirs_by_id.items():
|
||
assert elixir_id > 0
|
||
assert elixir.id == elixir_id
|
||
|
||
|
||
class TestGameDataAPI:
|
||
"""测试 API (API 测试通常在固定环境下运行,这里不使用多语言参数化以免影响 Server 状态)"""
|
||
|
||
@pytest.fixture
|
||
def client(self):
|
||
from fastapi.testclient import TestClient
|
||
from src.server.main import app
|
||
return TestClient(app)
|
||
|
||
def test_game_data_techniques_have_sect_id(self, client):
|
||
response = client.get("/api/meta/game_data")
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert len(data["techniques"]) > 0
|
||
assert "sect_id" in data["techniques"][0]
|
||
|
||
def test_game_data_sects_structure(self, client):
|
||
response = client.get("/api/meta/game_data")
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert len(data["sects"]) > 0
|
||
assert "id" in data["sects"][0]
|
||
|
||
if __name__ == "__main__":
|
||
pytest.main([__file__, "-v"])
|