test(ai-studio): add e2e integration checklist
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
- [任务看板(历史归档)](06-tasks.md):T-000~T-549 历史任务,已冻结,不再新增。
|
||||
- [模块 / CLI 合约](api.md):本地模块接口、Chrome 启动参数、账号配置 schema。
|
||||
- [界面与流程结构](routes.md):GUI 窗口、操作流程、按钮职责(无前端路由,用 GUI 流程替代)。
|
||||
- [AI工场端到端验收](ai-studio-e2e-checklist.md):⑥ AI工场 cmhub 托管主线的自动化覆盖、人工只读验收和发布检查。
|
||||
- [当前实现状态](current-state.md):当前代码现实、可运行命令、下一步可做任务。
|
||||
- [常见问题排查](troubleshooting.md):本地配置、启动报错、敏感文件修复等排障记录。
|
||||
- [产品与 UI 评估](ux-review.md):以 PM + UI 设计视角评估主流程模块 / 组件合理性,含优化方案与优先级清单。
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# AI工场端到端验收与发布检查
|
||||
|
||||
> T-595 阶段性收口文档。本文只记录 AI工场当前已实现的 cmhub 托管主线,不包含 BYOK、自定义 Provider、自动上传蝦皮或本地图片导入。
|
||||
|
||||
## 已实现闭环
|
||||
|
||||
当前 ⑥「AI工场」支持:
|
||||
|
||||
1. 按账号别名 + 商品 ID 创建/恢复项目。
|
||||
2. 后台只读打开蝦皮商品详情页,读取原主图 URL 并写入照片资产。
|
||||
3. 单击原主图下载到本地 `data/images/pool/<slug>/<item_id>/originals/`,并可作为源图。
|
||||
4. 使用完整提示词和 cmhub 托管生图别名创建 N 个独立 job。
|
||||
5. submit 后立即保存 `task_key/task_id/call_id/扣点/余额`;重启后继续查询时只 poll/download,不重复 submit。
|
||||
6. 成功图片进入照片池;失败/过期/停止任务在照片池任务行独立显示。
|
||||
7. 将照片拖入主图/详情图终选,可插入、重排、移出,同类别去重,跨类别允许复用。
|
||||
8. 允许主图/详情图未满数量时导出当前终选为 JPEG。
|
||||
9. 商品目录已存在时只允许覆盖受管命名文件、新建带时间目录或取消。
|
||||
|
||||
## 自动化覆盖
|
||||
|
||||
自动化不连接真实蝦皮或真实 cmhub,不产生费用。当前覆盖:
|
||||
|
||||
- SQLite:项目、资产、job、终选顺序。
|
||||
- 远程图片:公网 URL、大小/类型/解码校验、原图落盘幂等。
|
||||
- cmhub 托管:N 张独立 job、submit/poll/download、续查、下载失败恢复、单张失败不影响其它任务。
|
||||
- GUI:6 Tab 顺序、AI工场控件、照片池、终选拖放等价操作、导出入口、继续查询入口和计费状态。
|
||||
- 导出:部分导出、JPEG 真转码、透明铺白底、已存在目录三选、受管文件覆盖保护、源文件缺失失败不创建目标。
|
||||
- 端到端 mock:项目 → 原图 URL → 原图下载 → cmhub mock 生图 → 终选 → 导出。
|
||||
|
||||
推荐验证命令:
|
||||
|
||||
```powershell
|
||||
python -m ruff check app tests main.py
|
||||
py -3.10 -m compileall app main.py
|
||||
py -3.10 -m unittest discover -s tests
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 人工真实只读验收
|
||||
|
||||
需要本机存在已登录的测试账号 Chrome、可用 cmhub Key 和允许测试的商品 ID。人工验收时必须遵守:
|
||||
|
||||
- 只读拉取原主图,不点击蝦皮更新,不上传图片,不修改标题/封面。
|
||||
- 不把 API Key、Cookie、账号、真实商品图片或 `data/` 内容提交到仓库。
|
||||
- 真实 cmhub 小样本建议 1~2 张,确认扣点、余额、失败提示和继续查询即可,不做大批量费用测试。
|
||||
|
||||
建议步骤:
|
||||
|
||||
1. ④账号管理确认测试账号已登录。
|
||||
2. ⑥AI工场选择该账号和测试商品 ID,点击「打开项目」。
|
||||
3. 点击「拉取主图」,确认原主图列表出现,浏览器不进入编辑提交动作。
|
||||
4. 单击一张原主图,确认照片池出现本地可用源图。
|
||||
5. 输入短提示词,数量设为 1,点击「开始生成」。
|
||||
6. 生成成功后把图片拖入主图终选,点击「导出终选」到临时目录。
|
||||
7. 关闭软件后重开,确认项目、照片池、job 状态、终选顺序仍保留。
|
||||
8. 若中途停止或下载失败,点击「继续查询任务」,确认不重复 submit。
|
||||
|
||||
当前 T-595 自动化已完成;真实只读验收需要运行环境,不在仓库中伪造结果。
|
||||
|
||||
## 发布检查
|
||||
|
||||
发布前确认:
|
||||
|
||||
- `release/` 或 `dist/` 不包含 `data/`、真实 DB、真实图片、Chrome user-data-dir、日志或 cmhub Key。
|
||||
- ⑤设置页和 ⑥AI工场没有 OpenAI Key、Provider URL、direct backend 或“自定义模型”入口。
|
||||
- ⑥AI工场没有「导入本地图片」按钮或隐藏入口。
|
||||
- 运行日志、状态栏、弹窗不展示 API Key、Authorization、Cookie、完整请求体或 cmhub 内部接口路径。
|
||||
- 导出只写用户选择目录下的图片文件,不导出 prompt、task key、SQLite 数据或日志。
|
||||
+51
-4
@@ -354,21 +354,68 @@ render_prompt(template_text, task) -> str
|
||||
- `list_cover_templates()` 不会在启动时创建文件;只有保存/新建/另存为才写 `data/prompts/cover/*.txt`。
|
||||
- 模板名不可为空,不允许路径分隔符、`..` 或 Windows 非法文件名字符;重命名时目标重名会报错。
|
||||
|
||||
## AI工场模块(`app/image_studio*.py`,已建)
|
||||
|
||||
AI工场是 ⑥ Tab 的本地图片项目工作区,不自动上传蝦皮、不修改线上商品。
|
||||
|
||||
```python
|
||||
# app/image_studio.py
|
||||
ImageStudioProject / ImageStudioAsset / ImageStudioJob / ImageStudioSelection
|
||||
create_or_get_project(account_or_fields, item_id, ...) -> ImageStudioProject
|
||||
list_projects(path=None) -> list[ImageStudioProject]
|
||||
update_project_prompt(project_id, draft_prompt, path=None) -> ImageStudioProject
|
||||
sync_original_asset_urls(project_id, image_urls, path=None) -> list[ImageStudioAsset]
|
||||
list_assets(project_id, kind=None, include_missing=True, path=None) -> list[ImageStudioAsset]
|
||||
create_job(project_id, source_asset_id=None, job_type="main", prompt="", ...) -> ImageStudioJob
|
||||
list_resumable_jobs(project_id=None, include_failed_downloads=False, path=None) -> list[ImageStudioJob]
|
||||
replace_selections(project_id, selection_type, asset_ids, path=None) -> list[ImageStudioSelection]
|
||||
pull_remote_main_image_urls(account_or_alias, item_id, path=None, config=None) -> dict
|
||||
|
||||
# app/image_studio_images.py
|
||||
download_remote_image(url, max_bytes=..., timeout=(connect, read)) -> RemoteImage
|
||||
load_thumbnail(url, key=None, max_size=220) -> ThumbnailResult
|
||||
download_original_asset(asset_id, path=None, config=None) -> ImageStudioAsset
|
||||
|
||||
# app/image_studio_generation.py
|
||||
generate_image_jobs(project_id, source_asset_id, prompt, count, job_type="main", aspect_ratio="1:1", ...) -> dict
|
||||
resume_image_jobs(project_id=None, aspect_ratio="1:1", ...) -> dict
|
||||
|
||||
# app/image_studio_export.py
|
||||
export_project_selection(project_id, parent_dir, existing_mode="fail", path=None, config=None) -> ExportResult
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- 项目唯一键为账号别名 + 商品 ID;图片文件默认在 `data/images/pool/<slug>/<item_id>/` 下分 `originals/generated/exports`。
|
||||
- 拉取蝦皮原主图只读:复用 `editor.open_product(..., bring_to_front=False)` 和 `editor.read_product_image_urls()`,不上传、不拖拽、不点击更新。
|
||||
- 原图下载走 `image_studio_images` 的公网 URL、大小、Content-Type、重定向和 PIL 解码校验;只在用户单击时落盘。
|
||||
- cmhub 托管生图每张都是独立 job:保存 `task_key/task_id/status/call_id/points_cost/points_balance`;已有 `task_id` 时只 poll/download,不重复 submit。
|
||||
- `include_failed_downloads=True` 允许 failed 但已有 `task_id`、无输出 asset 的任务继续查询,用于下载失败或本地保存失败恢复。
|
||||
- 终选顺序由 `replace_selections()` 事务替换,主图/详情图同类别去重、跨类别可复用。
|
||||
- 导出只写 JPEG 图片文件,透明图铺白底;商品目录已存在时只能覆盖受管命名文件或新建带时间目录,不合并、不递归清空。
|
||||
- 所有真实网络/CDP/图片生成/下载/转码由 GUI worker 调用;Widget slot 不直接执行长耗时操作。
|
||||
|
||||
## gui 模块(`app/gui/` 包,已建,PySide6)
|
||||
|
||||
```python
|
||||
# GUI 入口
|
||||
main() -> int # 创建 QApplication + MainWindow
|
||||
class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤;支持注入 db_path/config/config_path/ai_models_path 便于测试
|
||||
class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤⑥;支持注入 db_path/config/config_path/ai_models_path 便于测试
|
||||
class CollectTab(QWidget) # ① 导入采集:导入 Excel + 汇总栏 + QTableView 任务列表 + 未匹配略过标记
|
||||
class GenerateTab(QWidget) # ② AI生成:提示词管理 + 筛选任务 + 生成封面图片成本开关 + 开始/停止生成 + 新旧封面预览 + AI生成运行日志
|
||||
class ApplyTab(QWidget) # ③ 更新蝦皮:筛选已生成任务 + 检查本轮更新 + 缺失内容校验 + 确认后分批真实更新 + 运行日志
|
||||
class SettingsTab(QWidget) # ⑤ 设置:cmhub 网关配置 + 响应式三列布局 + 角色/生成参数/路径端口 + 蝦皮更新安全 + 未保存状态追踪
|
||||
class ImageStudioTab(QWidget) # ⑥ AI工场:项目、原图、照片池、终选、cmhub托管生成、导出
|
||||
class CollectWorker(BaseWorker) # ① 后台采集:账号就绪预检 -> editor.collect -> db.set_collected/mark_skipped/mark_failed
|
||||
class GenerateWorker(BaseWorker) # ② 后台生成:ai.generate_batch -> db.set_generated/mark_failed + 进度
|
||||
class ApplyWorker(BaseWorker) # ③ 后台更新:账号就绪预检 -> 检查或按批调用 editor.apply_task(...) -> db.set_applied/mark_skipped
|
||||
class WriteBackWorker(BaseWorker) # ①/③ 后台回写:旧字段或更新结果写回原 Excel
|
||||
class AIModelTestWorker(BaseWorker) # ⑤ 后台测试 AI 模型连接:appconfig.test_ai_model
|
||||
class ImageStudioPullImagesWorker(BaseWorker) # ⑥ 后台只读拉蝦皮原主图 URL
|
||||
class ImageStudioDownloadOriginalWorker(BaseWorker)# ⑥ 后台下载远程原图
|
||||
class ImageStudioGenerateJobsWorker(BaseWorker) # ⑥ 后台提交/查询/下载 cmhub 生图 job
|
||||
class ImageStudioResumeJobsWorker(BaseWorker) # ⑥ 后台恢复已有 task_id 的生图 job
|
||||
class ImageStudioExportWorker(BaseWorker) # ⑥ 后台导出终选 JPEG
|
||||
class TaskTableModel(QAbstractTableModel) # 任务表格模型:账号/别名/商品ID/阶段;未匹配别名显示“略过”
|
||||
class GenerateTaskTableModel(QAbstractTableModel) # ② 任务表格模型:店铺/商品ID/旧标题/新标题/状态;generated/未提交/非运行中新标题可本地编辑
|
||||
class ApplyTaskTableModel(QAbstractTableModel) # ③ 任务表格模型:店铺/商品ID/新标题/新封面/阶段/结果;保持只读,重置更新状态走右键菜单
|
||||
@@ -378,11 +425,11 @@ TAB_TITLES: list[str] # 固定 Tab 顺序
|
||||
TAB_STYLE: str # 顶层 Tab 栏防误点样式:最小宽度/padding/间距/当前态
|
||||
```
|
||||
|
||||
T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负责旧导入路径兼容与 `main()`;`main_window.py` 放 `MainWindow`;`models.py` 放 3 个 TableModel;`widgets.py` 放色板、空状态卡、批次总览和日志 helper;`workers.py` 放具体 GUI worker;`tabs/` 下按 ①~⑤ 拆分各 Tab。对外仍保留 `from app import gui`、`from app.gui import MainWindow/CollectTab/GenerateWorker/...`。
|
||||
T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负责旧导入路径兼容与 `main()`;`main_window.py` 放 `MainWindow`;`models.py` 放 3 个 TableModel;`widgets.py` 放色板、空状态卡、批次总览和日志 helper;`workers.py` 放具体 GUI worker;`tabs/` 下按 ①~⑥ 拆分各 Tab。对外仍保留 `from app import gui`、`from app.gui import MainWindow/CollectTab/GenerateWorker/...`。
|
||||
|
||||
`MainWindow` 已实现五 Tab、① 导入采集任务列表、② AI生成布局/提示词/开始生成/停止/封面对照预览、③ 更新蝦皮筛选列表与检查/确认后分批真实更新、④ 账号管理、⑤ AI 模型管理。缺 PySide6 时 `main()` 返回 1 并输出明确提示。
|
||||
`MainWindow` 已实现六 Tab、① 导入采集任务列表、② AI生成布局/提示词/开始生成/停止/封面对照预览、③ 更新蝦皮筛选列表与检查/确认后分批真实更新、④ 账号管理、⑤ cmhub 设置、⑥ AI工场图片候选生成。缺 PySide6 时 `main()` 返回 1 并输出明确提示。
|
||||
|
||||
主 Tab 栏必须在 `MainWindow` 初始化时应用 `TAB_STYLE`:5 个 Tab 不使用 Qt 默认紧凑宽度,需保证点击区域稳定、间距清晰、当前 Tab 高亮明显。该样式属于全局导航基础,不归后续业务 Tab 任务重复实现。
|
||||
主 Tab 栏必须在 `MainWindow` 初始化时应用 `TAB_STYLE`:6 个 Tab 不使用 Qt 默认紧凑宽度,需保证点击区域稳定、间距清晰、当前 Tab 高亮明显。该样式属于全局导航基础,不归后续业务 Tab 任务重复实现。
|
||||
|
||||
④ 账号管理要点:
|
||||
|
||||
|
||||
+8
-2
@@ -3,7 +3,7 @@ id: T-595
|
||||
title: AI工场端到端集成、真实只读验收与产品文档收口
|
||||
phase: 7
|
||||
deps: [T-593, T-594]
|
||||
status: TODO
|
||||
status: DONE
|
||||
created: 2026-07-11
|
||||
---
|
||||
|
||||
@@ -39,4 +39,10 @@ AI工场横跨 DB、CDP、远程图片、cmhub 异步生成、GUI 拖放和本
|
||||
|
||||
## 执行记录
|
||||
|
||||
(完成后记录端到端场景、真实只读验收、截图和全量回归。)
|
||||
- 2026-07-11:完成 AI工场端到端集成收口。
|
||||
- 新增 `tests/test_image_studio_e2e.py`:mock 串起项目创建、原主图 URL 同步、原图下载、cmhub 托管生图、终选排序和 JPEG 导出,验证文件顺序与导出结果。
|
||||
- 新增 `docs/ai-studio-e2e-checklist.md`:记录已实现闭环、自动化覆盖、人工真实只读验收步骤和发布检查;明确不包含 BYOK、自定义 Provider、自动上传蝦皮或本地图片导入。
|
||||
- 更新 `docs/api.md`:补充 `image_studio.py`、`image_studio_images.py`、`image_studio_generation.py`、`image_studio_export.py` 合约,并把 GUI 合约更新为 6 Tab 和 AI工场 worker。
|
||||
- 更新 `docs/README.md`、`docs/ui/README.md`,登记 AI工场验收文档和 6 Tab/主流水线事实。
|
||||
- 验证:主工作区 targeted 测试通过;因无关默认提示词脏文件仍会影响全量 unittest,已在干净 worktree 仅套用 T-595 diff 后运行 `python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`(370 tests)和 `git diff --check`,全部通过。
|
||||
- 真实只读验收未执行:本轮没有使用本机真实已登录账号、真实商品 ID 或真实 cmhub Key;已在 `docs/ai-studio-e2e-checklist.md` 写明人工验收步骤,不伪造真实验收结果。
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
| 文件 | 内容 |
|
||||
| --- | --- |
|
||||
| [overview-pipeline.svg](overview-pipeline.svg) | 5 Tab 流水线总览(stage:imported→collected→generated→applied) |
|
||||
| [overview-pipeline.svg](overview-pipeline.svg) | 主流水线总览(stage:imported→collected→generated→applied;⑥ AI工场为图片候选工作区) |
|
||||
| [tab1-import-collect.svg](tab1-import-collect.svg) | ① 导入采集:导入 Excel、任务列表、采集旧标题/旧封面、回写、日志 |
|
||||
| [tab2-ai-generate.svg](tab2-ai-generate.svg) | ② AI生成:提示词、生成、新旧封面对照、重置图片后批量补生成 |
|
||||
| [tab2-ai-generate-T584.svg](tab2-ai-generate-T584.svg) | ② AI生成(T-584 提案):标题提示词加模板控件行、封面控件压一行、插入类按钮下移、标题输入框增高 |
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from _helpers import TempDirMixin
|
||||
|
||||
from app import db, image_studio, image_studio_export, image_studio_generation, image_studio_images
|
||||
|
||||
|
||||
class ImageStudioE2ETests(TempDirMixin, unittest.TestCase):
|
||||
def _png_bytes(self, color=(80, 130, 220)):
|
||||
from PIL import Image
|
||||
|
||||
output = io.BytesIO()
|
||||
Image.new("RGB", (32, 32), color).save(output, format="PNG")
|
||||
return output.getvalue()
|
||||
|
||||
def _config(self, temp_dir):
|
||||
return {
|
||||
"data_dir": temp_dir,
|
||||
"db_path": os.path.join(temp_dir, "cmshopee.db"),
|
||||
"image_dir": os.path.join(temp_dir, "images"),
|
||||
"ai": {
|
||||
"backend": "cmhub",
|
||||
"image_concurrency": 2,
|
||||
"retry": 0,
|
||||
"resolution": "1k",
|
||||
"jpg_quality": 90,
|
||||
"cmhub": {
|
||||
"base_url": "https://cmhub.example.com",
|
||||
"image_alias": "image-hd",
|
||||
"connect_timeout": 3,
|
||||
"download_with_curl": "false",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def _runtime(self):
|
||||
return {
|
||||
"base_url": "https://cmhub.example.com",
|
||||
"api_key": "sk-test",
|
||||
"alias": "image-hd",
|
||||
"connect_timeout": 3,
|
||||
"use_system_proxy": False,
|
||||
"download_with_curl": "false",
|
||||
}
|
||||
|
||||
def test_mock_ai_studio_project_to_export_flow(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self._config(temp_dir)
|
||||
db.init_db(cfg["db_path"])
|
||||
project = image_studio.create_or_get_project(
|
||||
account_alias="alias",
|
||||
account_slug="alias_slug",
|
||||
item_id="51100639510",
|
||||
draft_prompt="完整提示词",
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
original = image_studio.sync_original_asset_urls(
|
||||
project.id,
|
||||
[{"index": 1, "src": "https://cdn.example.com/original.png"}],
|
||||
path=cfg["db_path"],
|
||||
)[0]
|
||||
|
||||
remote = image_studio_images.RemoteImage(
|
||||
url=original.remote_url,
|
||||
content=self._png_bytes(),
|
||||
content_type="image/png",
|
||||
final_url=original.remote_url,
|
||||
redirected=False,
|
||||
)
|
||||
with mock.patch("app.image_studio_images.download_remote_image", return_value=remote):
|
||||
source = image_studio_images.download_original_asset(
|
||||
original.id,
|
||||
path=cfg["db_path"],
|
||||
config=cfg,
|
||||
)
|
||||
|
||||
submitted = []
|
||||
|
||||
def fake_submit(method, url, api_key, **kwargs):
|
||||
task_id = f"cmhub-task-{len(submitted) + 1}"
|
||||
submitted.append(task_id)
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"status": "queued",
|
||||
"call_id": f"call-{len(submitted)}",
|
||||
"points_cost": 2,
|
||||
"points_balance": 100 - len(submitted) * 2,
|
||||
}
|
||||
|
||||
def fake_poll(method, url, api_key, **kwargs):
|
||||
task_id = url.rsplit("/", 1)[-1]
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"status": "succeeded",
|
||||
"result": {"image_url": f"https://cdn.example.com/{task_id}.png"},
|
||||
}
|
||||
|
||||
with mock.patch("app.image_studio_generation._runtime", return_value=self._runtime()), \
|
||||
mock.patch(
|
||||
"app.image_studio_generation.ai._cmhub_call_with_retry",
|
||||
side_effect=fake_submit,
|
||||
), \
|
||||
mock.patch("app.image_studio_generation.ai._cmhub_call_once", side_effect=fake_poll), \
|
||||
mock.patch(
|
||||
"app.image_studio_generation.ai._download_cmhub_image_with_retry",
|
||||
return_value=(self._png_bytes(color=(10, 200, 120)), 0.1),
|
||||
):
|
||||
summary = image_studio_generation.generate_image_jobs(
|
||||
project.id,
|
||||
source.id,
|
||||
"完整提示词",
|
||||
2,
|
||||
config=cfg,
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
|
||||
self.assertEqual(2, summary["success"])
|
||||
generated = image_studio.list_assets(project.id, kind="generated_main", path=cfg["db_path"])
|
||||
self.assertEqual(2, len(generated))
|
||||
image_studio.replace_selections(
|
||||
project.id,
|
||||
"main",
|
||||
[asset.id for asset in generated],
|
||||
path=cfg["db_path"],
|
||||
)
|
||||
image_studio.replace_selections(project.id, "detail", [source.id], path=cfg["db_path"])
|
||||
|
||||
export_parent = os.path.join(temp_dir, "exports")
|
||||
os.makedirs(export_parent)
|
||||
result = image_studio_export.export_project_selection(
|
||||
project.id,
|
||||
export_parent,
|
||||
path=cfg["db_path"],
|
||||
config=cfg,
|
||||
)
|
||||
|
||||
self.assertEqual(2, result.main_count)
|
||||
self.assertEqual(1, result.detail_count)
|
||||
self.assertEqual(
|
||||
[
|
||||
"51100639510_主图_1.jpg",
|
||||
"51100639510_主图_2.jpg",
|
||||
"51100639510_详情图_1.jpg",
|
||||
],
|
||||
[os.path.basename(item.output_path) for item in result.files],
|
||||
)
|
||||
self.assertTrue(all(os.path.isfile(item.output_path) for item in result.files))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user