用户表的「商品id(货号)」整列为空时,行有效性判定(标题+货号+图三者非空) 把每一行都判为不完整 → read_all_rows 与 load_outfit_tasks 都返回 0:预览下拉 只剩占位符,且「开始生成」也判定无待处理行。货号不进提示词、目录行输出名沿用 源图名,对该用法是多余约束。 - excel_service.py: read_all_rows / load_outfit_tasks 完整性判定改为只要求 标题 + 衣服图路径 非空,货号可空(状态过滤不变) - core/ai_outfit.py: 单文件行货号为空时输出名回退 Path(garment).stem (货号非空仍用 货号.jpg);目录行不变 - ai_outfit_panel.py: 样本下拉标签货号为空显示「(无货号)」 - tests: 空货号行被 read_all_rows/load_outfit_tasks 收录、单文件空货号按源图名命名 - docs/11 §4(表+规则)/§9(命名回退)、tasks.md §19.14 离屏冒烟:标题填/货号空/C=目录 → 预览下拉有行、生成产出到子目录、E=完成; 全套测试在 Python 3.7 全绿(test_config_service 的失败属并行的出厂模板工作,与本次无关)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
144 lines
5.4 KiB
Python
144 lines
5.4 KiB
Python
"""Tests for AI outfit Excel service.
|
|
|
|
Pure service tests: no GUI dependency.
|
|
"""
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from openpyxl import Workbook, load_workbook
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
|
|
class TestOutfitExcelService(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = Path(tempfile.mkdtemp())
|
|
self.excel_path = self.tmp / "outfit.xlsx"
|
|
self._write_workbook()
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(str(self.tmp), ignore_errors=True)
|
|
|
|
def _write_workbook(self):
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws.append(["标题", "货号", "衣服图", "结果图", "状态", "原因"])
|
|
ws.append(["T恤", "TY001", r"D:\img\ty001.png", "", "", ""])
|
|
ws.append(["已完成", "TY002", r"D:\img\ty002.png", r"D:\out\ty002.jpg", "完成", ""])
|
|
ws.append(["失败行", "TY003", r"D:\img\ty003.png", "", "失败", "旧错误"])
|
|
ws.append(["缺路径", "TY004", "", "", "", ""])
|
|
ws.append([" 保留空格 ", 1005, r"D:\img\ty005.png", "", "", ""])
|
|
wb.save(str(self.excel_path))
|
|
wb.close()
|
|
|
|
def _read_row(self, row):
|
|
wb = load_workbook(str(self.excel_path))
|
|
try:
|
|
ws = wb.active
|
|
return (
|
|
ws.cell(row, 4).value,
|
|
ws.cell(row, 5).value,
|
|
ws.cell(row, 6).value,
|
|
)
|
|
finally:
|
|
wb.close()
|
|
|
|
def test_load_tasks_skips_done_failed_and_incomplete_by_default(self):
|
|
from services.excel_service import load_outfit_tasks
|
|
|
|
tasks = load_outfit_tasks(self.excel_path)
|
|
|
|
self.assertEqual([task.row_index for task in tasks], [2, 6])
|
|
self.assertEqual(tasks[0].title, "T恤")
|
|
self.assertEqual(tasks[0].product_id, "TY001")
|
|
self.assertEqual(tasks[0].garment_path, r"D:\img\ty001.png")
|
|
|
|
def test_load_tasks_includes_failed_when_retry_enabled(self):
|
|
from services.excel_service import load_outfit_tasks
|
|
|
|
tasks = load_outfit_tasks(self.excel_path, retry_failed=True)
|
|
|
|
self.assertEqual([task.row_index for task in tasks], [2, 4, 6])
|
|
self.assertEqual(tasks[1].status, "失败")
|
|
|
|
def test_load_tasks_preserves_user_cell_text(self):
|
|
from services.excel_service import load_outfit_tasks
|
|
|
|
tasks = load_outfit_tasks(self.excel_path)
|
|
task = tasks[-1]
|
|
|
|
self.assertEqual(task.title, " 保留空格 ")
|
|
self.assertEqual(task.product_id, "1005")
|
|
|
|
def test_write_success_result_updates_d_e_f_and_saves(self):
|
|
from core.models import OutfitResult, OutfitTask
|
|
from services.excel_service import write_outfit_result
|
|
|
|
task = OutfitTask(row_index=2, title="T恤", product_id="TY001", garment_path="g.png")
|
|
result = OutfitResult(task=task, success=True, output_path=r"D:\out\TY001.jpg")
|
|
|
|
write_outfit_result(self.excel_path, result)
|
|
|
|
self.assertEqual(self._read_row(2), (r"D:\out\TY001.jpg", "完成", None))
|
|
|
|
def test_write_failure_result_updates_e_f_and_saves(self):
|
|
from core.models import OutfitResult, OutfitTask
|
|
from services.excel_service import write_outfit_result
|
|
|
|
task = OutfitTask(row_index=2, title="T恤", product_id="TY001", garment_path="g.png")
|
|
result = OutfitResult(task=task, success=False, error="衣服图打不开")
|
|
|
|
write_outfit_result(self.excel_path, result)
|
|
|
|
self.assertEqual(self._read_row(2), (None, "失败", "衣服图打不开"))
|
|
|
|
def test_read_all_rows_includes_done_failed_skips_incomplete(self):
|
|
from services.excel_service import read_all_rows
|
|
|
|
rows = read_all_rows(self.excel_path)
|
|
|
|
# rows 2(待处理) 3(完成) 4(失败) 6(待处理) — row 5 (空衣服图) skipped
|
|
self.assertEqual([r.row_index for r in rows], [2, 3, 4, 6])
|
|
by_idx = {r.row_index: r for r in rows}
|
|
self.assertEqual(by_idx[3].status, "完成")
|
|
self.assertEqual(by_idx[4].status, "失败")
|
|
self.assertEqual(by_idx[6].title, " 保留空格 ") # raw text preserved
|
|
|
|
def test_empty_product_id_rows_are_included(self):
|
|
"""货号(B)可空:只要求 标题 + 衣服图路径(docs/11 §4)。"""
|
|
from services.excel_service import load_outfit_tasks, read_all_rows
|
|
|
|
p = self.tmp / "nopid.xlsx"
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws.append(["标题", "货号", "衣服图", "结果", "状态", "原因"])
|
|
ws.append(["有标题无货号", "", r"D:\img\a.png", "", "", ""]) # 收录
|
|
ws.append(["缺图也缺货号", "", "", "", "", ""]) # 缺图 → 仍跳过
|
|
wb.save(str(p))
|
|
wb.close()
|
|
|
|
tasks = load_outfit_tasks(p)
|
|
self.assertEqual([t.row_index for t in tasks], [2])
|
|
self.assertEqual(tasks[0].product_id, "")
|
|
self.assertEqual([r.row_index for r in read_all_rows(p)], [2])
|
|
|
|
def test_check_excel_writable_true_for_existing_workbook(self):
|
|
from services.excel_service import check_excel_writable
|
|
|
|
self.assertTrue(check_excel_writable(self.excel_path))
|
|
|
|
def test_ensure_excel_writable_raises_when_probe_fails(self):
|
|
from services.excel_service import ExcelInUseError, ensure_excel_writable
|
|
|
|
with patch("builtins.open", side_effect=OSError("locked")):
|
|
with self.assertRaises(ExcelInUseError):
|
|
ensure_excel_writable(self.excel_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|