From dc2a6e80b46a6825e22e574be057afa36a85a01a Mon Sep 17 00:00:00 2001 From: Hansong Qi Date: Thu, 24 Sep 2026 16:32:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(solaris):=20=E4=BF=AE=E5=A4=8D=20analyze=20?= =?UTF-8?q?--source-dir=20=E4=B8=8D=E7=94=9F=E6=95=88=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=20--package-name=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题一:-w/--source-dir 不生效。DataSourceDirSettings 的四个分类目录 (PATCH/HTML5/UNITY/FLASH)在类定义时按缺省 BASE_DIR 固化,构造后修改 BASE_DIR(-w/--source-dir 或 SOLARIS_DATA_BASE_DIR)不会传播,数据始终 从 ./source 读取。改为:分类目录未被显式指定(环境变量或构造参数)时 一律从当前 BASE_DIR 派生;CLI 改为构造时传入 BASE_DIR 而非构造后改属性。 问题二:--package-name 指定多个有交集的包时重复类导致拓扑排序误报 「循环依赖」。递归导入会让父包带上子包已有的分析器类(如 pet 包自带 element_type/peak_pool/peak_cost),CLI 逐包 extend 不去重, _topological_sort_analyzers 按 len(sorted) != len(analyzers) 判定失败。 parse 命令同款模式虽不报错,但会让同一 parser 执行两次。均按类身份去重。 真实复现场景:用公开的 config-sources 仓库跑 solaris analyze -w config-sources data,需改用 SOLARIS_DATA_* 环境变量; 只跑 pet/skill 子包则触发问题二。 --- packages/solaris/solaris/analyze/settings.py | 14 ++++-- packages/solaris/solaris/cli/analyze.py | 13 ++++-- packages/solaris/solaris/cli/parse.py | 7 ++- packages/solaris/tests/analyze/test_cli.py | 35 +++++++++++++++ .../analyze/test_data_source_settings.py | 45 +++++++++++++++++++ 5 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 packages/solaris/tests/analyze/test_cli.py create mode 100644 packages/solaris/tests/analyze/test_data_source_settings.py diff --git a/packages/solaris/solaris/analyze/settings.py b/packages/solaris/solaris/analyze/settings.py index cf2d65c..d29b62e 100644 --- a/packages/solaris/solaris/analyze/settings.py +++ b/packages/solaris/solaris/analyze/settings.py @@ -20,8 +20,14 @@ class DataSourceDirSettings(BaseSettings): @model_validator(mode='after') def combine_dirs(self) -> Self: - self.HTML5_DIR = self.HTML5_DIR - self.PATCH_DIR = self.PATCH_DIR - self.UNITY_DIR = self.UNITY_DIR - self.FLASH_DIR = self.FLASH_DIR + # 分类目录未显式指定(环境变量或构造参数)时,一律从当前 BASE_DIR 派生, + # 保证 BASE_DIR 来自 -w/--source-dir 或环境变量时子目录跟随变化 + for field_name, sub_dir in ( + ('PATCH_DIR', 'patch'), + ('HTML5_DIR', 'html5'), + ('UNITY_DIR', 'unity'), + ('FLASH_DIR', 'flash'), + ): + if field_name not in self.model_fields_set: + setattr(self, field_name, self.BASE_DIR / sub_dir) return self diff --git a/packages/solaris/solaris/cli/analyze.py b/packages/solaris/solaris/cli/analyze.py index 129848b..ee11e90 100644 --- a/packages/solaris/solaris/cli/analyze.py +++ b/packages/solaris/solaris/cli/analyze.py @@ -76,9 +76,15 @@ def analyze( """分析赛尔号客户端数据并生成API数据或文档。""" ctx.ensure_object(dict) + # 不同的包可能导出同一个分析器类(例如同时指定父包与其子包), + # 重复的类会让拓扑排序的入度统计失配,误报循环依赖 analyzer_classes: list[type[BaseAnalyzer]] = [] + seen_analyzers: set[type[BaseAnalyzer]] = set() for name in package_name: - analyzer_classes.extend(import_analyzer_classes(name)) + for analyzer_class in import_analyzer_classes(name): + if analyzer_class not in seen_analyzers: + seen_analyzers.add(analyzer_class) + analyzer_classes.append(analyzer_class) ctx.obj['analyzer_classes'] = analyzer_classes @@ -91,9 +97,10 @@ def analyze( ctx.exit() # 设置数据源目录 - settings = DataSourceDirSettings() if source_dir: - settings.BASE_DIR = Path(source_dir) + settings = DataSourceDirSettings(BASE_DIR=source_dir) + else: + settings = DataSourceDirSettings() DataImportConfig.set_source_dir(settings) ctx.obj['data_source_settings'] = settings diff --git a/packages/solaris/solaris/cli/parse.py b/packages/solaris/solaris/cli/parse.py index 8da9eed..c80bc8b 100644 --- a/packages/solaris/solaris/cli/parse.py +++ b/packages/solaris/solaris/cli/parse.py @@ -87,9 +87,14 @@ def parse( ) -> None: """批量解析赛尔号 Unity 端二进制数据""" + # 同 analyze:重复的包会让同一个 parser 被导入并执行多次 parser_classes = [] + seen_parsers: set[type[BaseParser]] = set() for name in package_name: - parser_classes.extend(import_parser_classes(name)) + for parser_class in import_parser_classes(name): + if parser_class not in seen_parsers: + seen_parsers.add(parser_class) + parser_classes.append(parser_class) if parser_names: parser_classes, not_found = filter_parser_classes(parser_classes, parser_names) diff --git a/packages/solaris/tests/analyze/test_cli.py b/packages/solaris/tests/analyze/test_cli.py new file mode 100644 index 0000000..714157b --- /dev/null +++ b/packages/solaris/tests/analyze/test_cli.py @@ -0,0 +1,35 @@ +from click.testing import CliRunner + +from solaris.cli.analyze import analyze + + +def _listed_analyzer_names(output: str) -> list[str]: + names = [] + for line in output.splitlines(): + if line.startswith('分析器: '): + names.append(line.removeprefix('分析器: ').split(' |')[0]) + return names + + +def test_overlapping_packages_do_not_duplicate_analyzers(): + """pet 包会递归导入 skill 包也导出的分析器,重复类曾让拓扑排序误报循环依赖""" + runner = CliRunner() + result = runner.invoke( + analyze, + [ + '--package-name', + 'solaris.analyze.analyzers.pet', + '--package-name', + 'solaris.analyze.analyzers.skill', + '--list-analyzers', + 'data', + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0 + analyzer_names = _listed_analyzer_names(result.output) + assert len(analyzer_names) == len(set(analyzer_names)) + # 两个包共有的分析器只保留一份 + assert analyzer_names.count('PetAdvanceAnalyzer') == 1 + assert 'SkillAnalyzer' in analyzer_names diff --git a/packages/solaris/tests/analyze/test_data_source_settings.py b/packages/solaris/tests/analyze/test_data_source_settings.py new file mode 100644 index 0000000..4f27701 --- /dev/null +++ b/packages/solaris/tests/analyze/test_data_source_settings.py @@ -0,0 +1,45 @@ +from pathlib import Path + +import pytest + +from solaris.analyze.settings import DataSourceDirSettings + + +@pytest.fixture(autouse=True) +def clear_solaris_data_env(monkeypatch): + """隔离开发机上可能存在的 SOLARIS_DATA_* 环境变量""" + for name in ( + 'SOLARIS_DATA_BASE_DIR', + 'SOLARIS_DATA_PATCH_DIR', + 'SOLARIS_DATA_HTML5_DIR', + 'SOLARIS_DATA_UNITY_DIR', + 'SOLARIS_DATA_FLASH_DIR', + ): + monkeypatch.delenv(name, raising=False) + + +def test_sub_dirs_derive_from_base_dir(): + settings = DataSourceDirSettings(BASE_DIR='custom') + + assert settings.PATCH_DIR == Path('custom') / 'patch' + assert settings.HTML5_DIR == Path('custom') / 'html5' + assert settings.UNITY_DIR == Path('custom') / 'unity' + assert settings.FLASH_DIR == Path('custom') / 'flash' + + +def test_default_base_dir_keeps_source_layout(): + settings = DataSourceDirSettings() + + assert settings.BASE_DIR == Path('./source') + assert settings.UNITY_DIR == Path('./source') / 'unity' + assert settings.PATCH_DIR == Path('./source') / 'patch' + + +def test_explicit_sub_dir_wins_over_base_dir(): + settings = DataSourceDirSettings( + BASE_DIR='custom', + UNITY_DIR='/elsewhere/unity', + ) + + assert settings.UNITY_DIR == Path('/elsewhere/unity') + assert settings.PATCH_DIR == Path('custom') / 'patch'