diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74bb294..42c4d96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,7 @@ jobs: python-version: "3.11" - name: Run tests - run: uv run pytest + run: uv run --group blender pytest tests/anyimage tests/tools - name: Pack extensions run: uv run pack diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbb0a56..f45b061 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,4 +23,4 @@ jobs: python-version: "3.11" - name: Run tests - run: uv run pytest + run: uv run --group blender pytest tests/anyimage tests/tools diff --git a/openspec/changes/split-capability-environments/.openspec.yaml b/openspec/changes/split-capability-environments/.openspec.yaml new file mode 100644 index 0000000..eaa6b1c --- /dev/null +++ b/openspec/changes/split-capability-environments/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-19 diff --git a/openspec/changes/split-capability-environments/design.md b/openspec/changes/split-capability-environments/design.md new file mode 100644 index 0000000..f08bcf6 --- /dev/null +++ b/openspec/changes/split-capability-environments/design.md @@ -0,0 +1,77 @@ +## Context + +项目当前只有一个默认 `dev` 依赖组。`uv run pytest` 因此会同步 Blender、AI 推理、模型导出和文档构建的全部工具,即使 CI 只需要运行不涉及真实模型的测试。测试也没有稳定的能力分类,工作流只能运行整个集合。 + +依赖和测试存在两条不同边界:产品中的 AI 调度、界面和协议可以通过 mock 在常规环境验证;真实模型实现、ONNX Runtime 行为及模型资产生产需要额外的大型依赖。分类应以执行所需能力为准,而不是看到 AI 业务名称就全部排除。 + +## Goals / Non-Goals + +**Goals:** + +- 依赖组名称表达可用能力,而不是单一测试用途。 +- CI 只同步核心与 Blender 能力所需依赖。 +- AI 模型执行测试由开发者在本地显式运行。 +- 模型资产生产和文档构建拥有独立环境。 +- 测试选择规则可见、可校验,新测试不易被意外遗漏。 + +**Non-Goals:** + +- 不改变扩展安装后的 AI 运行环境及依赖版本。 +- 不移除依赖 Blender 的 CI 覆盖。 +- 不下载真实模型权重作为测试前置条件。 +- 不修改产品功能、模型格式或打包产物。 + +## Decisions + +### 使用能力名称划分依赖组 + +依赖组采用以下职责: + +- `dev`:uv 默认加载的通用开发环境,包括 pytest 和通用图像测试依赖。 +- `blender`:Blender API、节点和几何验证所需的增量依赖。 +- `ai`:本地 AI 推理实现验证所需的增量依赖。 +- `models`:下载、转换和导出模型资产所需工具;它独立于日常 AI 推理验证。 +- `docs`:文档构建和发布工具。 + +选择能力名称可以让同一组依赖同时服务业务开发、调试和测试。备选方案 `test-core`、`test-ai` 会错误地把依赖限定为测试用途;继续把所有工具放在 `dev` 则无法减少 CI 下载。 + +### 以目录表达测试执行边界 + +将要求 AI 专用运行时的测试放入 `tests/ai`。CI 直接运行 `tests/anyimage` 和 `tests/tools`;本地 AI 验证直接运行 `tests/ai`;全量验证运行 `tests`。 + +AI 界面、Operator 调度、参数序列化、模型目录和下载流程等使用 mock 且不加载模型运行时的测试不标记为 `ai`,继续在 CI 中执行。这样排除的是昂贵执行环境,而不是 AI 业务覆盖。 + +目录名称直接表达执行环境,不增加 marker 和筛选表达式。实现时必须验证未安装 `ai` 和 `models` 组时,常规测试目录能够完成收集,避免顶层导入泄漏可选依赖。 + +### CI 显式选择环境 + +测试与发布工作流使用 `--group blender`,由 uv 自动加载默认 `dev`,并直接执行常规测试目录。打包只需默认 `dev`。所有命令均使用 uv 的默认组语义,不额外传入 `--no-default-groups`。 + +`dev` 只保存所有开发流程都需要的轻量依赖;大型能力必须留在显式组中。发布工作流采用同一测试命令,保证发布门禁与普通 CI 一致。 + +### 模型资产生产与 AI 运行分离 + +`torch`、`onnx` 及模型导出脚本的其他依赖归入 `models`;`onnxruntime` 和 AI 服务本地验证所需依赖归入 `ai`。只有两种能力确实共享的包才重复约束同一版本。 + +模型 CLI 测试若只验证调度和失败处理且使用 mock,可留在默认 `dev` 环境;只有实际导入或执行模型制作组件的测试进入 AI/模型相关本地集合。 + +## Risks / Trade-offs + +- [pytest 在筛选前导入测试模块,AI 顶层导入可能导致 CI 收集失败] → 将可选运行时导入保持在执行边界内,并用不安装 AI 组的干净环境验证收集与测试。 +- [测试放错目录使昂贵测试进入 CI] → 让 `tests/ai` 只承载真实依赖 AI 专用运行时的测试,并在干净 CI 环境验证常规目录。 +- [AI 业务回归失去 CI 保护] → mock 层协议与集成测试继续保留在常规业务目录。 +- [能力组之间重复依赖或版本漂移] → 复用默认 `dev` 环境,并由单一 `uv.lock` 锁定版本。 +- [本地全量测试命令变长] → 在项目常用命令和测试配置中形成唯一、明确的命令,不保留旧环境兼容层。 + +## Migration Plan + +1. 建立能力依赖组并刷新锁文件。 +2. 将 AI 模型执行测试移动到独立目录,确认不安装 AI 依赖时可收集常规测试目录。 +3. 分别运行核心、Blender、AI 和全量验证。 +4. 更新测试与发布工作流,确认 CI 的同步计划不包含 AI、模型和文档组。 + +若迁移失败,可在提交合并前恢复原 dependency group 与工作流;本变更不涉及用户数据或线上状态迁移。 + +## Open Questions + +无。实现阶段根据实际 import 链决定个别包归属,但不得改变上述能力边界。 diff --git a/openspec/changes/split-capability-environments/proposal.md b/openspec/changes/split-capability-environments/proposal.md new file mode 100644 index 0000000..112249e --- /dev/null +++ b/openspec/changes/split-capability-environments/proposal.md @@ -0,0 +1,28 @@ +## Why + +当前开发依赖全部集中在 `dev` 组,常规测试和 CI 会下载模型导出、ONNX 推理及文档构建等无关的大型依赖。需要按业务能力拆分环境,并让 AI 模型测试只在本地显式运行,以降低 CI 的下载量和执行成本。 + +## What Changes + +- 将开发依赖按默认 `dev` 及 `blender`、`ai`、`models`、`docs` 能力分组,不使用仅体现测试用途的 `test-*` 命名。 +- 将 AI 模型执行测试集中到独立目录,常规测试保留在现有业务目录。 +- CI 和发布流程只安装并运行默认开发与 Blender 集成所需环境,不安装 AI 推理和模型导出依赖。 +- 提供明确的本地 AI 测试与全量测试命令。 +- 保留不依赖真实模型运行时的 AI 界面、参数、任务协议和错误处理测试在 CI 中。 + +## Capabilities + +### New Capabilities + +- `capability-environments`: 定义开发依赖的能力分组、测试分类,以及 CI 与本地测试环境的选择规则。 + +### Modified Capabilities + +- `developer-tool-cli`: 调整项目测试命令,使核心、Blender、AI 和全量验证可以分别执行。 + +## Impact + +- 修改 `pyproject.toml` 的 dependency groups 和 pytest 配置,并刷新 `uv.lock`。 +- 调整测试标记及少量测试归属,不改变产品运行时代码和扩展依赖协议。 +- 修改 `.github/workflows/test.yml` 与 `.github/workflows/release.yml` 的依赖安装和测试命令。 +- 常规 CI 不再下载 `torch`、`onnx`、`onnxruntime`、`huggingface-hub` 和文档工具链;本地仍可运行 AI 与全量测试。 diff --git a/openspec/changes/split-capability-environments/specs/capability-environments/spec.md b/openspec/changes/split-capability-environments/specs/capability-environments/spec.md new file mode 100644 index 0000000..3ab3daa --- /dev/null +++ b/openspec/changes/split-capability-environments/specs/capability-environments/spec.md @@ -0,0 +1,45 @@ +## ADDED Requirements + +### Requirement: Capability-oriented dependency groups + +项目 SHALL 使用默认 `dev` 及 `blender`、`ai`、`models` 和 `docs` 依赖组分别表达通用开发、Blender 集成、AI 推理、模型资产生产和文档构建能力。非默认能力组 SHALL 仅包含其职责所需的增量依赖。 + +#### Scenario: Install Blender development environment +- **WHEN** 开发者或 CI 仅选择 `blender` 能力组 +- **THEN** uv 自动加载默认 `dev` 及 Blender 验证所需依赖,不安装 AI 推理、模型资产生产或文档构建专用依赖 + +#### Scenario: Install AI development environment +- **WHEN** 开发者选择 `ai` 能力组 +- **THEN** 环境包含本地 AI 推理及其测试所需依赖 + +#### Scenario: Install model production environment +- **WHEN** 开发者选择 `models` 能力组 +- **THEN** 环境包含模型下载、转换和导出所需依赖,且该组不成为常规 CI 的前置条件 + +### Requirement: Test categories follow execution requirements + +测试 SHALL 根据实际执行环境分目录。只有要求 AI 专用运行时或模型实现的测试 SHALL 位于 `tests/ai`;使用 mock 验证 AI 界面、任务协议、参数传递和失败处理的测试 SHALL 留在常规业务测试目录。 + +#### Scenario: Run CI test set +- **WHEN** 在只安装 `blender` 及其基础能力的环境执行常规测试目录 +- **THEN** 测试收集和执行成功,且不要求 AI 或模型资产生产专用依赖 + +#### Scenario: Run local AI test set +- **WHEN** 开发者安装 `ai` 能力并执行 `tests/ai` +- **THEN** 只执行明确归类的 AI 模型运行与实现测试 + +#### Scenario: Protect mocked AI integration +- **WHEN** AI 相关测试不加载真实模型运行时且只验证产品集成契约 +- **THEN** 该测试包含在常规 CI 集合中 + +### Requirement: Continuous integration excludes local-only capabilities + +普通测试工作流和发布工作流 SHALL 使用默认 `dev`、显式选择 Blender 能力并直接运行常规测试目录,不得同步 `ai`、`models` 或 `docs` 能力组。项目命令 SHALL 不要求关闭默认依赖组。 + +#### Scenario: Pull request validation +- **WHEN** 测试工作流在 pull request 或主分支运行 +- **THEN** 工作流完成非 AI 测试,且依赖同步不包含 AI、模型生产和文档专用包 + +#### Scenario: Release validation +- **WHEN** 发布工作流构建扩展包 +- **THEN** 发布前运行与普通 CI 相同的非 AI 测试门禁,不运行本地 AI 测试 diff --git a/openspec/changes/split-capability-environments/specs/developer-tool-cli/spec.md b/openspec/changes/split-capability-environments/specs/developer-tool-cli/spec.md new file mode 100644 index 0000000..45115f0 --- /dev/null +++ b/openspec/changes/split-capability-environments/specs/developer-tool-cli/spec.md @@ -0,0 +1,17 @@ +## ADDED Requirements + +### Requirement: Capability-specific test commands + +项目 SHALL 提供明确的 uv 命令分别运行常规 CI 测试、本地 AI 测试和本地全量测试。命令 SHALL 使用默认 `dev` 并显式增加所需能力组,不要求传入 `--no-default-groups`。 + +#### Scenario: Run regular validation +- **WHEN** 开发者运行常规验证命令 +- **THEN** 命令安装核心与 Blender 能力并执行所有非 AI 测试 + +#### Scenario: Run AI validation locally +- **WHEN** 开发者运行 AI 验证命令 +- **THEN** 命令安装 AI 能力并仅执行 AI 测试 + +#### Scenario: Run full validation locally +- **WHEN** 开发者运行全量验证命令 +- **THEN** 命令安装 Blender 与 AI 能力并执行全部测试 diff --git a/openspec/changes/split-capability-environments/tasks.md b/openspec/changes/split-capability-environments/tasks.md new file mode 100644 index 0000000..73b7728 --- /dev/null +++ b/openspec/changes/split-capability-environments/tasks.md @@ -0,0 +1,23 @@ +## 1. 拆分能力环境 + +- [x] 1.1 将 `pyproject.toml` 拆为默认 `dev` 及 `blender`、`ai`、`models` 和 `docs` 增量能力组,并保持现有版本约束 +- [x] 1.2 检查模型 CLI、打包和节点工具的真实 import 链,修正依赖归属并确保各能力组可独立同步 +- [x] 1.3 刷新 `uv.lock`,确认常规 Blender 环境不包含 AI、模型生产和文档专用包 + +## 2. 分类测试 + +- [x] 2.1 将模型实现、推理和后处理测试移动到独立的 `tests/ai` 目录 +- [x] 2.2 保留使用 mock 的 AI 界面、Operator、任务协议、模型目录、下载和失败处理测试在非 AI 集合 +- [x] 2.3 消除非 AI 测试收集阶段对 AI 与模型生产专用依赖的顶层导入 +- [x] 2.4 更新测试命令相关测试,使常规、AI 和全量验证的能力组与 pytest 选择条件受到覆盖 + +## 3. 精简 CI + +- [x] 3.1 修改 `.github/workflows/test.yml`,只选择 `blender` 能力并直接运行常规测试目录 +- [x] 3.2 修改 `.github/workflows/release.yml`,使用与普通 CI 一致的测试环境和常规测试目录 + +## 4. 验证 + +- [x] 4.1 在不安装 `ai`、`models` 和 `docs` 能力的环境运行全部常规测试目录 +- [x] 4.2 在本地 AI 环境运行 AI 测试,并在 Blender 与 AI 能力同时启用时运行全量测试 +- [x] 4.3 检查最终差异和 uv 依赖树,确认 CI 路径不会下载 `torch`、`onnx`、`onnxruntime`、`huggingface-hub` 或文档工具链 diff --git a/pyproject.toml b/pyproject.toml index d087bd0..f06171f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,21 +9,30 @@ dependencies = ["blendjob==0.1.17", "numpy>=1.26,<2.0", "scipy==1.15.3"] [dependency-groups] dev = [ + "pillow==12.1.1", + "pip>=25,<27", + "pytest", +] +blender = [ "bpy==4.5.3", +] +ai = [ "huggingface-hub==1.4.1", + "onnxruntime==1.24.4", + "trimesh==4.11.2", +] +models = [ + "onnx==1.19.0", + "onnxruntime==1.24.4", + "torch==2.13.0", +] +docs = [ "mike>=2.1.3,<3", "mkdocs>=1.6.1,<2", "mkdocs-click==0.9.0", "mkdocs-material>=9.6.16,<10", "mkdocs-static-i18n[material]>=1.3.0", "mkdocstrings[python]>=1.0.6", - "onnx==1.19.0", - "onnxruntime==1.24.4", - "pillow==12.1.1", - "pip>=25,<27", - "pytest", - "torch==2.13.0", - "trimesh==4.11.2", ] [project.scripts] diff --git a/tests/ai/__init__.py b/tests/ai/__init__.py new file mode 100644 index 0000000..43ee973 --- /dev/null +++ b/tests/ai/__init__.py @@ -0,0 +1 @@ +"""AI inference tests run locally.""" diff --git a/tests/anyimage/server/test_background_alpha.py b/tests/ai/test_background_alpha.py similarity index 100% rename from tests/anyimage/server/test_background_alpha.py rename to tests/ai/test_background_alpha.py diff --git a/tests/anyimage/server/test_background_models.py b/tests/ai/test_background_models.py similarity index 100% rename from tests/anyimage/server/test_background_models.py rename to tests/ai/test_background_models.py diff --git a/tests/anyimage/server/test_geometry_artifacts.py b/tests/ai/test_geometry_artifacts.py similarity index 100% rename from tests/anyimage/server/test_geometry_artifacts.py rename to tests/ai/test_geometry_artifacts.py diff --git a/tests/anyimage/server/test_model_ben2_onnx.py b/tests/ai/test_model_ben2_onnx.py similarity index 100% rename from tests/anyimage/server/test_model_ben2_onnx.py rename to tests/ai/test_model_ben2_onnx.py diff --git a/tests/anyimage/server/test_model_birefnet_onnx.py b/tests/ai/test_model_birefnet_onnx.py similarity index 100% rename from tests/anyimage/server/test_model_birefnet_onnx.py rename to tests/ai/test_model_birefnet_onnx.py diff --git a/tests/anyimage/server/test_model_geometry.py b/tests/ai/test_model_geometry.py similarity index 100% rename from tests/anyimage/server/test_model_geometry.py rename to tests/ai/test_model_geometry.py diff --git a/tests/anyimage/server/test_model_inference.py b/tests/ai/test_model_inference.py similarity index 100% rename from tests/anyimage/server/test_model_inference.py rename to tests/ai/test_model_inference.py diff --git a/tests/anyimage/server/test_model_manager.py b/tests/ai/test_model_manager.py similarity index 100% rename from tests/anyimage/server/test_model_manager.py rename to tests/ai/test_model_manager.py diff --git a/tests/anyimage/server/test_model_moge2_onnx.py b/tests/ai/test_model_moge2_onnx.py similarity index 100% rename from tests/anyimage/server/test_model_moge2_onnx.py rename to tests/ai/test_model_moge2_onnx.py diff --git a/tests/anyimage/server/test_model_moge3_onnx.py b/tests/ai/test_model_moge3_onnx.py similarity index 100% rename from tests/anyimage/server/test_model_moge3_onnx.py rename to tests/ai/test_model_moge3_onnx.py diff --git a/tests/anyimage/server/test_model_onnx_runtime.py b/tests/ai/test_model_onnx_runtime.py similarity index 100% rename from tests/anyimage/server/test_model_onnx_runtime.py rename to tests/ai/test_model_onnx_runtime.py diff --git a/tests/anyimage/server/test_model_upscale_onnx.py b/tests/ai/test_model_upscale_onnx.py similarity index 100% rename from tests/anyimage/server/test_model_upscale_onnx.py rename to tests/ai/test_model_upscale_onnx.py diff --git a/tests/anyimage/server/test_panorama.py b/tests/ai/test_panorama.py similarity index 100% rename from tests/anyimage/server/test_panorama.py rename to tests/ai/test_panorama.py diff --git a/tests/anyimage/server/test_prediction_artifacts.py b/tests/ai/test_prediction_artifacts.py similarity index 100% rename from tests/anyimage/server/test_prediction_artifacts.py rename to tests/ai/test_prediction_artifacts.py diff --git a/tests/anyimage/server/test_server_jobs.py b/tests/ai/test_server_jobs.py similarity index 100% rename from tests/anyimage/server/test_server_jobs.py rename to tests/ai/test_server_jobs.py diff --git a/tests/tools/test_environments.py b/tests/tools/test_environments.py new file mode 100644 index 0000000..f059262 --- /dev/null +++ b/tests/tools/test_environments.py @@ -0,0 +1,56 @@ +import tomllib + +from tests.support.paths import PROJECT_ROOT + + +def project_configuration(): + return tomllib.loads( + (PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + + +def group_packages(configuration, name): + groups = configuration["dependency-groups"] + packages = set() + for item in groups[name]: + if isinstance(item, dict): + packages.update(group_packages(configuration, item["include-group"])) + else: + packages.add(item.split("==", 1)[0].split(">=", 1)[0]) + return packages + + +def test_capability_groups_keep_ci_dependencies_small(): + configuration = project_configuration() + groups = configuration["dependency-groups"] + assert set(groups) == {"dev", "blender", "ai", "models", "docs"} + ci_packages = group_packages(configuration, "dev") | group_packages( + configuration, "blender" + ) + assert ci_packages.isdisjoint( + {"huggingface-hub", "onnx", "onnxruntime", "torch"} + ) + + +def test_ai_and_model_groups_own_their_runtime_dependencies(): + configuration = project_configuration() + assert {"huggingface-hub", "onnxruntime"} <= group_packages( + configuration, "ai" + ) + assert {"onnx", "onnxruntime", "torch"} <= group_packages( + configuration, "models" + ) + + +def test_workflows_run_the_regular_blender_suite(): + command = "uv run --group blender pytest tests/anyimage tests/tools" + for name in ("test.yml", "release.yml"): + workflow = (PROJECT_ROOT / ".github" / "workflows" / name).read_text( + encoding="utf-8" + ) + assert command in workflow + + release = (PROJECT_ROOT / ".github" / "workflows" / "release.yml").read_text( + encoding="utf-8" + ) + assert "uv run pack" in release diff --git a/tools/models/exports.py b/tools/models/exports.py index a8ef920..e7dfe99 100644 --- a/tools/models/exports.py +++ b/tools/models/exports.py @@ -14,7 +14,7 @@ def _require_build_dependencies(): except ImportError as error: raise RuntimeError( "Model build dependencies are missing. Run with: " - "uv sync" + "uv sync --group models" ) from error return onnx, onnxruntime, torch diff --git a/uv.lock b/uv.lock index b4af88b..2b9ff78 100644 --- a/uv.lock +++ b/uv.lock @@ -22,22 +22,31 @@ dependencies = [ ] [package.dev-dependencies] -dev = [ - { name = "bpy" }, +ai = [ { name = "huggingface-hub" }, + { name = "onnxruntime" }, + { name = "trimesh" }, +] +blender = [ + { name = "bpy" }, +] +dev = [ + { name = "pillow" }, + { name = "pip" }, + { name = "pytest" }, +] +docs = [ { name = "mike" }, { name = "mkdocs" }, { name = "mkdocs-click" }, { name = "mkdocs-material" }, { name = "mkdocs-static-i18n", extra = ["material"] }, { name = "mkdocstrings", extra = ["python"] }, +] +models = [ { name = "onnx" }, { name = "onnxruntime" }, - { name = "pillow" }, - { name = "pip" }, - { name = "pytest" }, { name = "torch" }, - { name = "trimesh" }, ] [package.metadata] @@ -48,22 +57,29 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [ - { name = "bpy", specifier = "==4.5.3" }, +ai = [ { name = "huggingface-hub", specifier = "==1.4.1" }, + { name = "onnxruntime", specifier = "==1.24.4" }, + { name = "trimesh", specifier = "==4.11.2" }, +] +blender = [{ name = "bpy", specifier = "==4.5.3" }] +dev = [ + { name = "pillow", specifier = "==12.1.1" }, + { name = "pip", specifier = ">=25,<27" }, + { name = "pytest" }, +] +docs = [ { name = "mike", specifier = ">=2.1.3,<3" }, { name = "mkdocs", specifier = ">=1.6.1,<2" }, { name = "mkdocs-click", specifier = "==0.9.0" }, { name = "mkdocs-material", specifier = ">=9.6.16,<10" }, { name = "mkdocs-static-i18n", extras = ["material"], specifier = ">=1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.6" }, +] +models = [ { name = "onnx", specifier = "==1.19.0" }, { name = "onnxruntime", specifier = "==1.24.4" }, - { name = "pillow", specifier = "==12.1.1" }, - { name = "pip", specifier = ">=25,<27" }, - { name = "pytest" }, { name = "torch", specifier = "==2.13.0" }, - { name = "trimesh", specifier = "==4.11.2" }, ] [[package]]