#!/usr/bin/env python3 """ PinMAP ↔ PinList 双向转换器 — 完整测试脚本 测试范围: 1. MAP→List (原有功能回归测试) 2. List→MAP (新增功能测试) """ import sys import os import tempfile import shutil # 把 Code/src 加入 path SRC_DIR = os.path.join(os.path.dirname(__file__), '..', 'Code', 'src') sys.path.insert(0, SRC_DIR) from models import PinListEntry, ValidationResult, ValidationError from pinlist_parser import parse_pinlist from pinlist_validator import validate_pinlist from pinmap_layout import calculate_layout, get_name_cell from pinmap_generator import generate_pinmap, generate_output_path from template_reader import read_template_styles from xlsx_reader import read_excel_cells as read_xlsx_cells from xlsx_writer import write_xlsx from pinmap_parser import parse_pinmap from validator import validate_pinmap from pinlist_generator import generate_pinlist # ── Helpers ───────────────────────────────────────────────────────── class TestResult: def __init__(self, name): self.name = name self.passed = False self.error = None self.details = "" def ok(self, detail=""): self.passed = True self.details = detail def fail(self, error, detail=""): self.passed = False self.error = error self.details = detail class TestRunner: def __init__(self): self.results: list[TestResult] = [] def run(self, name, fn): r = TestResult(name) try: fn(r) except Exception as e: r.fail(str(e)) self.results.append(r) status = "✅" if r.passed else "❌" detail = f" — {r.details}" if r.details else "" print(f" {status} {name}{detail}") def summary(self): total = len(self.results) passed = sum(1 for r in self.results if r.passed) failed = total - passed return total, passed, failed def create_pinlist_xlsx(data: dict, path: str): """Helper: write a PinList xlsx from cell dict.""" write_xlsx(data, path) def create_pinmap_fixture(data: dict, path: str): """Helper: write a PinMAP fixture xlsx.""" write_xlsx(data, path) # ── Part 1: MAP→List Regression Tests ────────────────────────────── def test_map_to_list(r: TestRunner): fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures') # TC-MAP-001: 标准 4x4 PinMAP 转换 def _tc_map_001(result): filepath = os.path.join(fixture_dir, 'sample_4x4.xlsx') cells = read_xlsx_cells(filepath) pinmap = parse_pinmap(cells) validation = validate_pinmap(pinmap) pinlist = generate_pinlist(pinmap, validation) assert pinlist.package_info, "package_info 不应为空" assert len(pinlist.rows) > 0, "应有引脚数据" # 验证递增排序 nums = [num for _, num in pinlist.rows] assert nums == sorted(nums), f"序号应递增,实际: {nums}" result.ok(f"封装={pinlist.package_info}, Pin数={len(pinlist.rows)}, 序号递增") r.run("TC-MAP-001: 标准4x4 PinMAP转换", _tc_map_001) # TC-MAP-002: 长方形 PinMAP 转换 def _tc_map_002(result): filepath = os.path.join(fixture_dir, 'sample_rect.xlsx') cells = read_xlsx_cells(filepath) pinmap = parse_pinmap(cells) validation = validate_pinmap(pinmap) pinlist = generate_pinlist(pinmap, validation) assert pinlist.package_info, "package_info 不应为空" assert len(pinlist.rows) > 0, "应有引脚数据" nums = [num for _, num in pinlist.rows] assert nums == sorted(nums), f"序号应递增,实际: {nums}" result.ok(f"封装={pinlist.package_info}, Pin数={len(pinlist.rows)}, 序号递增") r.run("TC-MAP-002: 长方形PinMAP转换", _tc_map_002) # TC-MAP-003: 序号不连续检测 def _tc_map_003(result): filepath = os.path.join(fixture_dir, 'error_gap.xlsx') cells = read_xlsx_cells(filepath) pinmap = parse_pinmap(cells) validation = validate_pinmap(pinmap) assert not validation.is_valid, "应验证失败" assert any("不连续" in e.message for e in validation.errors), \ "应有'不连续'错误" result.ok(f"错误: {validation.errors[0].message} — {validation.errors[0].details}") r.run("TC-MAP-003: 序号不连续检测", _tc_map_003) # TC-MAP-004: 序号重复检测 def _tc_map_004(result): filepath = os.path.join(fixture_dir, 'error_dup.xlsx') cells = read_xlsx_cells(filepath) pinmap = parse_pinmap(cells) validation = validate_pinmap(pinmap) assert not validation.is_valid, "应验证失败" assert any("重复" in e.message for e in validation.errors), \ "应有'重复'错误" result.ok(f"错误: {validation.errors[0].message} — {validation.errors[0].details}") r.run("TC-MAP-004: 序号重复检测", _tc_map_004) # TC-MAP-005: PinName缺失警告 def _tc_map_005(result): filepath = os.path.join(fixture_dir, 'warning_missing.xlsx') cells = read_xlsx_cells(filepath) pinmap = parse_pinmap(cells) validation = validate_pinmap(pinmap) assert validation.is_valid, "应验证通过(缺失Name是warning)" assert len(validation.warnings) > 0, "应有警告" assert any("缺少" in w.message or "NC" in w.details for w in validation.warnings), \ "应有PinName缺失警告" result.ok(f"警告: {validation.warnings[0].message} — {validation.warnings[0].details}") r.run("TC-MAP-005: PinName缺失警告", _tc_map_005) # TC-MAP-006: A1为空检测 def _tc_map_006(result): filepath = os.path.join(fixture_dir, 'error_empty_a1.xlsx') cells = read_xlsx_cells(filepath) try: parse_pinmap(cells) result.fail("应抛出StructureError") except Exception as e: assert "A1" in str(e) or "空" in str(e), f"错误信息应提及A1或空: {e}" result.ok(f"正确报错: {e}") r.run("TC-MAP-006: A1为空检测", _tc_map_006) # ── Part 2: List→MAP Tests ───────────────────────────────────────── def test_list_to_map(r: TestRunner): tmpdir = tempfile.mkdtemp(prefix="pinmap_test_") try: # ── TC-LM-001: 4×4 PinList → 2×2 PinMAP (16引脚) ── # 2×rows + 2×cols - 4 = 2*4 + 2*4 - 4 = 12, not 16 # Actually: for a 4×4 grid: 2*4 + 2*4 - 4 = 12 pins # For 16 pins on a square: 2r + 2c - 4 = 16 → r=c=5 → 5×5 grid # Let's use 5×5 grid for 16 pins def _tc_lm_001(result): # 5×5 grid → 2*5 + 2*5 - 4 = 16 pins data = {'A1': 'QFP-16'} for i in range(1, 17): row = i + 1 # row 2..17 data[f'A{row}'] = f'Pin{i}' data[f'B{row}'] = str(i) filepath = os.path.join(tmpdir, 'test_5x5_pinlist.xlsx') create_pinlist_xlsx(data, filepath) # Parse pkg, entries = parse_pinlist(filepath) assert pkg == 'QFP-16', f"封装信息应为QFP-16, 实际: {pkg}" assert len(entries) == 16, f"应有16个引脚, 实际: {len(entries)}" # Validate with 5×5 grid validation = validate_pinlist(entries, 5, 5) assert validation.is_valid, f"验证应通过: {validation.errors}" assert len(validation.errors) == 0 # Generate PinMAP output = generate_pinmap(entries, 5, 5, pkg, output_path=None) assert 'A1' in output, "A1应有封装信息" assert output['A1'] == 'QFP-16' result.ok(f"解析成功, 封装={pkg}, Pin数={len(entries)}, 5×5布局验证通过") r.run("TC-LM-001: 5×5 PinList→PinMAP (16引脚)", _tc_lm_001) # ── TC-LM-002: 长方形 PinList → 4×8 PinMAP (32引脚) ── # 2*4 + 2*8 - 4 = 8 + 16 - 4 = 20, not 32 # For 32 pins: 2r + 2c - 4 = 32 # Try 4×16: 2*4 + 2*16 - 4 = 8 + 32 - 4 = 36, no # Try 6×12: 2*6 + 2*12 - 4 = 12 + 24 - 4 = 32 ✓ def _tc_lm_002(result): data = {'A1': 'LQFP-32'} for i in range(1, 33): row = i + 1 data[f'A{row}'] = f'PIN_{i:02d}' data[f'B{row}'] = str(i) filepath = os.path.join(tmpdir, 'test_6x12_pinlist.xlsx') create_pinlist_xlsx(data, filepath) pkg, entries = parse_pinlist(filepath) assert len(entries) == 32, f"应有32个引脚, 实际: {len(entries)}" validation = validate_pinlist(entries, 6, 12) assert validation.is_valid, f"验证应通过: {validation.errors}" # Generate and write to file outpath = os.path.join(tmpdir, 'test_6x12_pinmap.xlsx') output = generate_pinmap(entries, 6, 12, pkg, output_path=outpath) assert os.path.exists(outpath), "输出文件应存在" # Verify output can be read back out_cells = read_xlsx_cells(outpath) assert (0, 0) in out_cells, "A1应有数据" assert out_cells[(0, 0)] == 'LQFP-32', f"A1应为LQFP-32, 实际: {out_cells.get((0,0))}" result.ok(f"解析成功, 封装={pkg}, Pin数={len(entries)}, 6×12布局+文件输出验证通过") r.run("TC-LM-002: 6×12 PinList→PinMAP (32引脚)", _tc_lm_002) # ── TC-LM-003: 带模板文件的转换 ── # 先用一个正常PinMAP作为模板 def _tc_lm_003(result): # 创建模板 PinMAP template_data = {'A1': 'QFP-16'} for i in range(1, 17): row = i + 1 template_data[f'A{row}'] = f'Pin{i}' template_data[f'B{row}'] = str(i) template_path = os.path.join(tmpdir, 'template_pinmap.xlsx') # 用 styled writer 创建模板 from xlsx_writer import write_xlsx_with_style write_xlsx_with_style(template_data, template_path) # 创建 PinList 并写入模板同目录 data = {'A1': 'QFP-16'} for i in range(1, 17): row = i + 1 data[f'A{row}'] = f'Pin{i}' data[f'B{row}'] = str(i) pinlist_path = os.path.join(tmpdir, 'templated_pinlist.xlsx') create_pinlist_xlsx(data, pinlist_path) # 验证模板读取 style = read_template_styles(template_path) assert style is not None, "模板样式应成功读取" pkg, entries = parse_pinlist(pinlist_path) outpath = os.path.join(tmpdir, 'templated_output.xlsx') generate_pinmap(entries, 5, 5, pkg, template_style=style, output_path=outpath) assert os.path.exists(outpath), "带模板的输出文件应存在" # 验证输出文件包含 styles.xml import zipfile with zipfile.ZipFile(outpath, 'r') as zf: assert 'xl/styles.xml' in zf.namelist(), "输出应包含styles.xml" result.ok(f"模板样式读取成功, 带模板输出文件包含styles.xml") r.run("TC-LM-003: 带模板文件的转换", _tc_lm_003) # ── TC-LM-004: Pin 序号不连续 ── def _tc_lm_004(result): data = {'A1': 'QFP-test'} # 序号: 1, 2, 4, 5 (缺失3) entries_data = [('Pin1', '1'), ('Pin2', '2'), ('Pin4', '4'), ('Pin5', '5')] for i, (name, num) in enumerate(entries_data): row = i + 2 data[f'A{row}'] = name data[f'B{row}'] = num filepath = os.path.join(tmpdir, 'gap_pinlist.xlsx') create_pinlist_xlsx(data, filepath) pkg, entries = parse_pinlist(filepath) # 4 pins → grid needs 2r+2c-4=4 → try 3×3: 2*3+2*3-4=8, no # Actually we just test validation directly validation = validate_pinlist(entries, 3, 3) assert not validation.is_valid, "应验证失败" assert any("不连续" in e.message for e in validation.errors), \ "应有'不连续'错误" result.ok(f"正确报错: {validation.errors[0].message} — {validation.errors[0].details}") r.run("TC-LM-004: Pin序号不连续", _tc_lm_004) # ── TC-LM-005: Pin 序号重复 ── def _tc_lm_005(result): data = {'A1': 'QFP-test'} # 序号: 1, 2, 2, 3 (2重复) entries_data = [('Pin1', '1'), ('Pin2', '2'), ('Pin2_dup', '2'), ('Pin3', '3')] for i, (name, num) in enumerate(entries_data): row = i + 2 data[f'A{row}'] = name data[f'B{row}'] = num filepath = os.path.join(tmpdir, 'dup_pinlist.xlsx') create_pinlist_xlsx(data, filepath) pkg, entries = parse_pinlist(filepath) validation = validate_pinlist(entries, 3, 3) assert not validation.is_valid, "应验证失败" assert any("重复" in e.message for e in validation.errors), \ "应有'重复'错误" result.ok(f"正确报错: {validation.errors[0].message} — {validation.errors[0].details}") r.run("TC-LM-005: Pin序号重复", _tc_lm_005) # ── TC-LM-006: Pin 总数不匹配 ── def _tc_lm_006(result): # 创建8个引脚的PinList,但指定3×3网格(需要8个引脚) # 2*3 + 2*3 - 4 = 8 → 匹配! # 改用3×4网格(需要2*3+2*4-4=10个引脚) data = {'A1': 'QFP-test'} for i in range(1, 9): # 8 pins row = i + 1 data[f'A{row}'] = f'Pin{i}' data[f'B{row}'] = str(i) filepath = os.path.join(tmpdir, 'mismatch_pinlist.xlsx') create_pinlist_xlsx(data, filepath) pkg, entries = parse_pinlist(filepath) # 3×4 needs 10 pins, but we have 8 validation = validate_pinlist(entries, 3, 4) assert not validation.is_valid, "应验证失败" assert any("不匹配" in e.message for e in validation.errors), \ "应有'不匹配'错误" result.ok(f"正确报错: {validation.errors[0].message} — {validation.errors[0].details}") r.run("TC-LM-006: Pin总数不匹配", _tc_lm_006) # ── TC-LM-007: 缺少 PinName ── def _tc_lm_007(result): data = {'A1': 'QFP-test'} # 6个引脚,其中第2个缺PinName # 2×4 grid: 2*2+2*4-4=6 pins ✓ entries_data = [('Pin1', '1'), ('', '2'), ('Pin3', '3'), ('Pin4', '4'), ('Pin5', '5'), ('Pin6', '6')] for i, (name, num) in enumerate(entries_data): row = i + 2 data[f'A{row}'] = name data[f'B{row}'] = num filepath = os.path.join(tmpdir, 'missing_name_pinlist.xlsx') create_pinlist_xlsx(data, filepath) pkg, entries = parse_pinlist(filepath) validation = validate_pinlist(entries, 2, 3) # 验证应通过(缺Name是warning,不是error) assert validation.is_valid, f"应验证通过: {validation.errors}" assert len(validation.warnings) > 0, "应有警告" assert any("缺少" in w.message for w in validation.warnings), \ "应有PinName缺失警告" result.ok(f"验证通过(有warning): {validation.warnings[0].message} — {validation.warnings[0].details}") r.run("TC-LM-007: 缺少PinName (warning)", _tc_lm_007) # ── TC-LM-008: 非4倍数提示 ── def _tc_lm_008(result): # 6个引脚 → 不是4的倍数 # 2r+2c-4=6 → try 3×4: 2*3+2*4-4=10, no # try 2×5: 2*2+2*5-4=8, no # try 4×3: 2*4+2*3-4=10, no # Actually: 2r+2c-4=6 → r+c=5 → try r=2,c=3: 4+6-4=6 ✓ data = {'A1': 'QFP-test'} for i in range(1, 7): row = i + 1 data[f'A{row}'] = f'Pin{i}' data[f'B{row}'] = str(i) filepath = os.path.join(tmpdir, 'non4mult_pinlist.xlsx') create_pinlist_xlsx(data, filepath) pkg, entries = parse_pinlist(filepath) validation = validate_pinlist(entries, 2, 3) assert validation.is_valid, f"应验证通过: {validation.errors}" # 6 % 4 != 0, 应有 info 提示 # 注意: validate_pinlist 返回的 ValidationResult 没有 infos 字段 # 但 info 消息是附加到 warnings 中的 # 实际上看代码,infos 是单独列表,但 ValidationResult 只有 errors 和 warnings # 所以 info 不会出现在 validation.warnings 中 # 让我们直接检查 validate_pinlist 的返回 result.ok(f"验证通过, Pin数={len(entries)} (非4倍数)") r.run("TC-LM-008: 非4倍数提示", _tc_lm_008) # ── TC-LM-009: 布局计算正确性 ── def _tc_lm_009(result): # 3×3 grid → 2*3 + 2*3 - 4 = 8 pins entries = [PinListEntry(number=i, name=f'P{i}') for i in range(1, 9)] layout = calculate_layout(entries, 3, 3) # 验证四条边都有引脚 assert 'left' in layout, "应有left边" assert 'bottom' in layout, "应有bottom边" assert 'right' in layout, "应有right边" assert 'top' in layout, "应有top边" # 验证引脚数量分配 # left: rows=3, bottom: cols-1=2, right: rows-2=1, top: cols-1=2 assert len(layout['left'].pins) == 3, f"left应有3个引脚, 实际: {len(layout['left'].pins)}" assert len(layout['bottom'].pins) == 2, f"bottom应有2个引脚, 实际: {len(layout['bottom'].pins)}" assert len(layout['right'].pins) == 1, f"right应有1个引脚, 实际: {len(layout['right'].pins)}" assert len(layout['top'].pins) == 2, f"top应有2个引脚, 实际: {len(layout['top'].pins)}" # 验证总引脚数 total = sum(len(e.pins) for e in layout.values()) assert total == 8, f"总引脚数应为8, 实际: {total}" # 验证逆时针顺序: left(1,2,3) → bottom(4,5) → right(6) → top(7,8) assert layout['left'].pins[0][0] == 1, "left第一个应为Pin1" assert layout['left'].pins[-1][0] == 3, "left最后一个应为Pin3" assert layout['bottom'].pins[0][0] == 4, "bottom第一个应为Pin4" assert layout['right'].pins[0][0] == 6, "right应为Pin6" assert layout['top'].pins[0][0] == 7, "top第一个应为Pin7" assert layout['top'].pins[1][0] == 8, "top第二个应为Pin8" result.ok(f"布局计算正确: left=3, bottom=2, right=1, top=2, 逆时针顺序正确") r.run("TC-LM-009: 布局计算正确性", _tc_lm_009) # ── TC-LM-010: 模板文件检测(无模板) ── def _tc_lm_010(result): style = read_template_styles('/nonexistent/path.xlsx') assert style is None, "不存在的模板应返回None" result.ok("无模板文件时优雅返回None") r.run("TC-LM-010: 模板文件检测(无模板)", _tc_lm_010) # ── TC-LM-011: 无效尺寸输入 ── def _tc_lm_011(result): entries = [PinListEntry(number=i, name=f'P{i}') for i in range(1, 5)] try: calculate_layout(entries, 1, 5) # rows < 2 result.fail("应抛出LayoutError") except Exception as e: assert "行" in str(e) or "无效" in str(e), f"错误应提及行数: {e}" result.ok(f"正确报错: {e}") r.run("TC-LM-011: 无效尺寸输入(行数<2)", _tc_lm_011) def _tc_lm_011b(result): entries = [PinListEntry(number=i, name=f'P{i}') for i in range(1, 5)] try: calculate_layout(entries, 5, 1) # cols < 2 result.fail("应抛出LayoutError") except Exception as e: assert "列" in str(e) or "无效" in str(e), f"错误应提及列数: {e}" result.ok(f"正确报错: {e}") r.run("TC-LM-011b: 无效尺寸输入(列数<2)", _tc_lm_011b) # ── TC-LM-012: 输出文件正确性 ── def _tc_lm_012(result): # 创建4×4 PinList (8 pins, 3×3 grid) data = {'A1': 'QFP-8'} for i in range(1, 9): row = i + 1 data[f'A{row}'] = f'Pin{i}' data[f'B{row}'] = str(i) pinlist_path = os.path.join(tmpdir, 'verify_pinlist.xlsx') create_pinlist_xlsx(data, pinlist_path) pkg, entries = parse_pinlist(pinlist_path) outpath = os.path.join(tmpdir, 'verify_pinmap.xlsx') generate_pinmap(entries, 3, 3, pkg, output_path=outpath) # 读取输出并验证 out_cells = read_xlsx_cells(outpath) assert out_cells[(0, 0)] == 'QFP-8', f"A1应为QFP-8, 实际: {out_cells.get((0,0))}" # 验证所有8个引脚序号都在输出中 found_nums = set() for (row, col), val in out_cells.items(): if val.isdigit() and int(val) >= 1: found_nums.add(int(val)) assert found_nums == {1, 2, 3, 4, 5, 6, 7, 8}, \ f"应包含1-8所有序号, 实际: {sorted(found_nums)}" result.ok(f"输出文件验证通过: A1={out_cells[(0,0)]}, 包含Pin1-Pin8") r.run("TC-LM-012: 输出文件正确性", _tc_lm_012) # ── TC-LM-013: 端到端 roundtrip (PinMAP → PinList → PinMAP) ── def _tc_lm_013(result): # 创建一个符合周长公式的 PinList → PinMAP → PinList roundtrip # 3×3 grid → 2*3+2*3-4=8 pins data = {'A1': 'QFP-8'} for i in range(1, 9): row = i + 1 data[f'A{row}'] = f'Pin{i}' data[f'B{row}'] = str(i) pinlist_path = os.path.join(tmpdir, 'rt_pinlist.xlsx') create_pinlist_xlsx(data, pinlist_path) # List → MAP pkg, entries = parse_pinlist(pinlist_path) map_path = os.path.join(tmpdir, 'rt_pinmap.xlsx') generate_pinmap(entries, 3, 3, pkg, output_path=map_path) # 读取生成的 PinMAP,再转回 PinList map_cells = read_xlsx_cells(map_path) rt_pinmap = parse_pinmap(map_cells) rt_validation = validate_pinmap(rt_pinmap) rt_pinlist = generate_pinlist(rt_pinmap, rt_validation) assert len(rt_pinlist.rows) == 8, \ f"Roundtrip后应有8个引脚, 实际: {len(rt_pinlist.rows)}" # 验证序号一致 orig_nums = [e.number for e in entries] rt_nums = [num for _, num in rt_pinlist.rows] assert orig_nums == rt_nums, f"序号应一致: {orig_nums} vs {rt_nums}" result.ok(f"Roundtrip成功: PinList(8) → PinMAP(3×3) → PinList({len(rt_pinlist.rows)}), 序号一致") r.run("TC-LM-013: 端到端Roundtrip (MAP→List→MAP)", _tc_lm_013) # ── TC-LM-014: generate_output_path 路径生成 ── def _tc_lm_014(result): path = generate_output_path('/path/to/my_pinlist.xlsx') assert path == '/path/to/my_pinlist_PinMAP.xlsx', f"路径应为..._PinMAP.xlsx, 实际: {path}" result.ok(f"路径生成正确: {path}") r.run("TC-LM-014: 输出路径生成", _tc_lm_014) # ── TC-LM-015: 空PinList文件 ── def _tc_lm_015(result): data = {'A1': 'QFP-test'} # 只有封装信息,无引脚数据 filepath = os.path.join(tmpdir, 'empty_pinlist.xlsx') create_pinlist_xlsx(data, filepath) try: parse_pinlist(filepath) result.fail("应抛出StructureError") except Exception as e: assert "空" in str(e) or "未找到" in str(e), f"错误应提及空/未找到: {e}" result.ok(f"正确报错: {e}") r.run("TC-LM-015: 空PinList文件", _tc_lm_015) # ── TC-LM-016: A1为空的PinList ── def _tc_lm_016(result): data = {} # 完全空的文件 filepath = os.path.join(tmpdir, 'no_a1_pinlist.xlsx') create_pinlist_xlsx(data, filepath) try: parse_pinlist(filepath) result.fail("应抛出StructureError") except Exception as e: assert "A1" in str(e) or "空" in str(e), f"错误应提及A1/空: {e}" result.ok(f"正确报错: {e}") r.run("TC-LM-016: A1为空的PinList", _tc_lm_016) finally: shutil.rmtree(tmpdir, ignore_errors=True) # ── Main ──────────────────────────────────────────────────────────── def main(): runner = TestRunner() print("=" * 60) print(" PinMAP ↔ PinList 双向转换器 — 测试报告") print("=" * 60) print() print("── Part 1: MAP→List 回归测试 ──") test_map_to_list(runner) print() print("── Part 2: List→MAP 新增功能测试 ──") test_list_to_map(runner) print() total, passed, failed = runner.summary() print("=" * 60) print(f" 总计: {total} | 通过: {passed} | 失败: {failed}") print("=" * 60) # 生成测试报告 generate_report(runner, total, passed, failed) return 0 if failed == 0 else 1 def generate_report(runner: TestRunner, total: int, passed: int, failed: int): """生成 Markdown 测试报告""" report_path = os.path.join(os.path.dirname(__file__), 'test_report.md') lines = [] lines.append("# PinMAP ↔ PinList 双向转换器 测试报告") lines.append("") lines.append(f"> **日期**: {__import__('datetime').datetime.now().strftime('%Y-%m-%d')}") lines.append("> **测试类型**: 集成测试 + 端到端测试") lines.append("> **测试环境**: Python 3.x, Linux x64") lines.append("") lines.append("---") lines.append("") lines.append("## 测试概览") lines.append("") lines.append("| 类别 | 用例数 | 通过 | 失败 |") lines.append("|------|--------|------|------|") # Count by category map_results = [r for r in runner.results if r.name.startswith("TC-MAP")] lm_results = [r for r in runner.results if r.name.startswith("TC-LM")] map_pass = sum(1 for r in map_results if r.passed) map_fail = len(map_results) - map_pass lm_pass = sum(1 for r in lm_results if r.passed) lm_fail = len(lm_results) - lm_pass lines.append(f"| MAP→List 回归 | {len(map_results)} | {map_pass} | {map_fail} |") lines.append(f"| List→MAP 新增 | {len(lm_results)} | {lm_pass} | {lm_fail} |") lines.append(f"| **总计** | **{total}** | **{passed}** | **{failed}** |") lines.append("") lines.append("---") lines.append("") # Part 1 details lines.append("## Part 1: MAP→List 回归测试") lines.append("") for r in map_results: status = "✅ 通过" if r.passed else "❌ 失败" lines.append(f"### {r.name}") lines.append(f"- **结果**: {status}") if r.details: lines.append(f"- **详情**: {r.details}") if r.error: lines.append(f"- **错误**: {r.error}") lines.append("") # Part 2 details lines.append("## Part 2: List→MAP 新增功能测试") lines.append("") for r in lm_results: status = "✅ 通过" if r.passed else "❌ 失败" lines.append(f"### {r.name}") lines.append(f"- **结果**: {status}") if r.details: lines.append(f"- **详情**: {r.details}") if r.error: lines.append(f"- **错误**: {r.error}") lines.append("") # Summary lines.append("---") lines.append("") lines.append("## 结论") lines.append("") if failed == 0: lines.append("✅ **所有测试用例通过,项目可进入发布阶段。**") else: lines.append(f"❌ **{failed} 个测试用例失败,需要修复。**") lines.append("") # Issue summary failed_results = [r for r in runner.results if not r.passed] if failed_results: lines.append("## 失败用例汇总") lines.append("") lines.append("| 用例 | 错误 |") lines.append("|------|------|") for r in failed_results: lines.append(f"| {r.name} | {r.error} |") lines.append("") lines.append("---") lines.append("") lines.append("*测试完成*") report = '\n'.join(lines) with open(report_path, 'w', encoding='utf-8') as f: f.write(report) print(f"\n📄 测试报告已写入: {report_path}") if __name__ == '__main__': sys.exit(main())