Files
dcalc/backend/tests/unit/test_character_generation_refs.py

98 lines
4.4 KiB
Python

"""职业生成与更新保持稳定引用,并保留手写计算规则。"""
import ast
import json
from pathlib import Path
import runpy
import pytest
ROOT = Path(__file__).resolve().parents[3]
MERGER = runpy.run_path(str(ROOT / 'scripts/migrate_generated_characters.py'))
IMPORT = 'from core.character.refs.extra.creator import CreatorSkillRef as Ref\n'
def merge(tmp_path, runtime, generated):
old, new = tmp_path / 'runtime.py', tmp_path / 'generated.py'
old.write_text(runtime)
new.write_text(generated)
return MERGER['merge'](new, old)
def block(name='名称', ref='Ref.CREATESPACE', uuid='uuid1', number=0, extra=''):
return (f'class Skill{number}(PassiveSkill):\n name = {name!r}\n'
+ (f' ref = {ref}\n' if ref else '')
+ (f' uuid = {uuid!r}\n' if uuid else '')
+ ' position = 1\n cellPos = (1, 1)\n' + extra + '\n')
def test_generate_class_uses_shared_member_normalization(monkeypatch):
scripts = ROOT / 'backend/openapi/scripts'
monkeypatch.syspath_prepend(str(scripts))
gen = runpy.run_path(str(scripts / 'generate.py'))
job = {'name': 'gunner_female', 'jobId': 'job'}
profession = {'name': 'launcher_female'}
base = ROOT / 'backend/openapi/static/data/cn/gunner_female/launcher_female'
row = next(x for x in json.loads((base / 'skill_tree.json').read_text()) if x['code'] == 'apg-63')
detail = json.loads((base / 'skillDetail' / f"{row['skillId']}.json").read_text())
skill = gen['convert_data'](detail, row['skillId'], job)
output = gen['generate_class'](skill, 0, row, job, profession)
assert 'ref = Ref.APG_63' in output
ast.parse(output)
with pytest.raises(ValueError, match='code'):
gen['generate_class'](skill, 0, {**row, 'code': None}, job, profession)
def test_renamed_skill_matches_ref_and_keeps_rules_and_alias(tmp_path):
runtime = IMPORT.replace('as Ref', 'as OldRef') + block('旧名称', 'OldRef.CREATESPACE', number=27,
extra=' associate = ["手写规则"]\n level_source = OldRef.THETREEOFSEFIROT\n def custom(self):\n return 42\n')
generated = IMPORT + block('新名称', extra=' data0 = [0, 10]\n')
output, stats = merge(tmp_path, runtime, generated)
assert stats['matched'] == 1
assert 'class Skill27(' in output
assert 'ref = OldRef.CREATESPACE' in output
assert 'level_source = OldRef.THETREEOFSEFIROT' in output
assert 'return 42' in output and '手写规则' in output
assert "name = '旧名称'" in output
assert 'data0 = [0, 10]' in output
ast.parse(output)
def test_old_uuid_matches_after_rename_and_gets_ref_import(tmp_path):
output, stats = merge(tmp_path, block('旧名称', ref=None), IMPORT + block('新名称'))
assert stats['matched'] == 1
assert IMPORT.strip() in output
assert 'ref = Ref.CREATESPACE' in output
output2, _ = merge(tmp_path, output, IMPORT + block('新名称'))
assert output == output2
def test_runtime_only_skills_survive_and_new_skills_stay_commented(tmp_path):
old = IMPORT + block() + block('手写技能', ref='Ref.BACKSTEP', uuid='manual', number=9,
extra=' def effect(self, old, new):\n return 42\n')
new = IMPORT + block() + block('新增技能', ref='Ref.THETREEOFSEFIROT', uuid='new', number=1)
output, stats = merge(tmp_path, old, new)
assert stats['runtime_only'] == stats['commented'] == 1
assert 'class Skill9(' in output and 'return 42' in output
assert '# class Skill1(' in output
assert len([x for x in ast.parse(output).body if isinstance(x, ast.ClassDef)]) == 2
@pytest.mark.parametrize(('old_ref', 'old_uuid', 'new_ref', 'new_uuid'), [
('Ref.CREATESPACE', 'one', 'Ref.THETREEOFSEFIROT', 'one'),
(None, 'one', None, 'two'),
])
def test_identity_conflicts_do_not_fall_back_to_name(tmp_path, old_ref, old_uuid, new_ref, new_uuid):
with pytest.raises(ValueError, match='冲突'):
merge(tmp_path, IMPORT + block(ref=old_ref, uuid=old_uuid), IMPORT + block(ref=new_ref, uuid=new_uuid))
def test_duplicate_refs_fail_before_merge(tmp_path):
with pytest.raises(ValueError, match='duplicate ref_key'):
merge(tmp_path, IMPORT + block() + block('另一个技能', uuid='uuid2', number=1), IMPORT + block())
def test_legacy_name_fallback_without_identity(tmp_path):
output, stats = merge(tmp_path, block(ref=None, uuid=None), block(ref=None, uuid=None))
assert stats['matched'] == 1
ast.parse(output)