v9.5: 實作標籤完全不重疊算法

- 新增 _calculate_lane_conflicts_v2() 分開返回標籤重疊和線穿框分數
- 修改泳道選擇算法,優先選擇無標籤重疊的泳道
- 兩階段搜尋:優先側別無可用泳道則嘗試另一側
- 增強日誌輸出,顯示標籤範圍和詳細衝突分數

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
beabigegg
2025-11-06 11:35:29 +08:00
commit 2d37d23bcf
83 changed files with 22971 additions and 0 deletions

245
tests/unit/test_importer.py Normal file
View File

@@ -0,0 +1,245 @@
"""
CSV/XLSX 匯入模組單元測試
對應 TDD.md - UT-IMP-01: 匯入 CSV 欄位解析
驗證重點:
- 欄位自動對應
- 格式容錯
- 錯誤處理
Version: 1.0.0
DocID: TDD-UT-IMP-001
Related: SDD-API-001 (POST /import)
"""
import pytest
import os
from pathlib import Path
from datetime import datetime
from backend.schemas import Event, ImportResult, EventType
from backend.importer import CSVImporter, FieldMapper, DateParser, ColorValidator
# 測試資料路徑
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures"
SAMPLE_CSV = FIXTURES_DIR / "sample_events.csv"
INVALID_CSV = FIXTURES_DIR / "invalid_dates.csv"
class TestFieldMapper:
"""欄位映射器測試"""
def test_map_english_fields(self):
"""測試英文欄位映射"""
headers = ['id', 'title', 'start', 'end', 'group', 'description', 'color']
mapping = FieldMapper.map_fields(headers)
assert mapping['id'] == 'id'
assert mapping['title'] == 'title'
assert mapping['start'] == 'start'
assert mapping['end'] == 'end'
def test_map_chinese_fields(self):
"""測試中文欄位映射"""
headers = ['編號', '標題', '開始', '結束', '群組']
mapping = FieldMapper.map_fields(headers)
assert mapping['id'] == '編號'
assert mapping['title'] == '標題'
assert mapping['start'] == '開始'
def test_validate_missing_fields(self):
"""測試缺少必要欄位驗證"""
mapping = {'id': 'id', 'title': 'title'} # 缺少 start
missing = FieldMapper.validate_required_fields(mapping)
assert 'start' in missing
class TestDateParser:
"""日期解析器測試"""
def test_parse_standard_format(self):
"""測試標準日期格式"""
result = DateParser.parse('2024-01-01 09:00:00')
assert result == datetime(2024, 1, 1, 9, 0, 0)
def test_parse_date_only(self):
"""測試僅日期格式"""
result = DateParser.parse('2024-01-01')
assert result.year == 2024
assert result.month == 1
assert result.day == 1
def test_parse_slash_format(self):
"""測試斜線格式"""
result = DateParser.parse('2024/01/01')
assert result.year == 2024
def test_parse_invalid_date(self):
"""測試無效日期"""
result = DateParser.parse('invalid-date')
assert result is None
def test_parse_empty_string(self):
"""測試空字串"""
result = DateParser.parse('')
assert result is None
class TestColorValidator:
"""顏色驗證器測試"""
def test_validate_valid_hex(self):
"""測試有效的 HEX 顏色"""
result = ColorValidator.validate('#3B82F6')
assert result == '#3B82F6'
def test_validate_hex_without_hash(self):
"""測試不含 # 的 HEX 顏色"""
result = ColorValidator.validate('3B82F6')
assert result == '#3B82F6'
def test_validate_invalid_color(self):
"""測試無效顏色,應返回預設顏色"""
result = ColorValidator.validate('invalid')
assert result.startswith('#')
assert len(result) == 7
def test_validate_empty_color(self):
"""測試空顏色,應返回預設顏色"""
result = ColorValidator.validate('', 0)
assert result == ColorValidator.DEFAULT_COLORS[0]
class TestCSVImporter:
"""CSV 匯入器測試類別"""
def test_import_valid_csv(self):
"""
UT-IMP-01-001: 測試匯入有效的 CSV 檔案
預期結果:
- 成功解析所有行
- 欄位正確對應
- 日期格式正確轉換
"""
importer = CSVImporter()
result = importer.import_file(str(SAMPLE_CSV))
assert result.success is True
assert result.imported_count == 6
assert len(result.events) == 6
assert result.events[0].title == "專案啟動"
assert isinstance(result.events[0].start, datetime)
def test_import_with_invalid_dates(self):
"""
UT-IMP-01-003: 測試日期格式錯誤的 CSV
預期結果:
- 部分成功匯入
- 錯誤行記錄在 errors 列表中
"""
importer = CSVImporter()
result = importer.import_file(str(INVALID_CSV))
assert result.success is True
assert len(result.errors) > 0
# 應該有錯誤但不會完全失敗
def test_import_nonexistent_file(self):
"""測試匯入不存在的檔案"""
importer = CSVImporter()
result = importer.import_file('nonexistent.csv')
assert result.success is False
assert len(result.errors) > 0
assert result.imported_count == 0
def test_field_auto_mapping(self):
"""
UT-IMP-01-005: 測試欄位自動對應功能
測試不同的欄位名稱變體是否能正確對應
"""
# 建立臨時測試 CSV
test_csv = FIXTURES_DIR / "test_mapping.csv"
with open(test_csv, 'w', encoding='utf-8') as f:
f.write("ID,Title,Start\n")
f.write("1,Test Event,2024-01-01\n")
importer = CSVImporter()
result = importer.import_file(str(test_csv))
assert result.success is True
assert len(result.events) == 1
assert result.events[0].id == "1"
assert result.events[0].title == "Test Event"
# 清理
if test_csv.exists():
test_csv.unlink()
def test_color_format_validation(self):
"""
UT-IMP-01-007: 測試顏色格式驗證
預期結果:
- 有效的 HEX 顏色被接受
- 無效的顏色格式使用預設值
"""
importer = CSVImporter()
result = importer.import_file(str(SAMPLE_CSV))
assert result.success is True
# 所有事件都應該有有效的顏色
for event in result.events:
assert event.color.startswith('#')
assert len(event.color) == 7
def test_import_empty_csv(self):
"""測試匯入空白 CSV"""
# 建立空白測試 CSV
empty_csv = FIXTURES_DIR / "empty.csv"
with open(empty_csv, 'w', encoding='utf-8') as f:
f.write("")
importer = CSVImporter()
result = importer.import_file(str(empty_csv))
assert result.success is False
assert "" in str(result.errors[0])
# 清理
if empty_csv.exists():
empty_csv.unlink()
def test_date_format_tolerance(self):
"""
UT-IMP-01-006: 測試日期格式容錯
測試多種日期格式是否能正確解析
"""
# 建立測試 CSV with various date formats
test_csv = FIXTURES_DIR / "test_dates.csv"
with open(test_csv, 'w', encoding='utf-8') as f:
f.write("id,title,start\n")
f.write("1,Event1,2024-01-01\n")
f.write("2,Event2,2024/01/02\n")
f.write("3,Event3,2024-01-03 10:00:00\n")
importer = CSVImporter()
result = importer.import_file(str(test_csv))
assert result.success is True
assert result.imported_count == 3
assert all(isinstance(e.start, datetime) for e in result.events)
# 清理
if test_csv.exists():
test_csv.unlink()
if __name__ == "__main__":
pytest.main([__file__, "-v"])