# -*- coding: utf-8 -*- """k1_harness CLI. python -m k1_harness selftest python -m k1_harness run --suite app --transport com --port COM5 python -m k1_harness run --suite app --transport ble python -m k1_harness run --suite ble python -m k1_harness run --suite keys,packs,ui --device AT32F403AC python -m k1_harness run --suite all --profile nightly """ from __future__ import annotations import argparse import os import sys from dataclasses import dataclass from datetime import datetime from .paths import default_report_dir from .report import HarnessReport from .suites import SUITES, ALL_ORDER @dataclass class Ctx: transport: str = "ble" port: str = "" baud: int = 115200 device: str = "AT32F403AC" allow_poweroff: bool = False dump_png: str = "" def _run_selftest() -> int: report = HarnessReport(title="K1 harness selftest(无硬件)") # 1) 协议帧编解码自检(复用现有 selftest 逻辑,零硬件) import io import contextlib import test_protocol_app_sim as appsim buf = io.StringIO() with contextlib.redirect_stdout(buf): code = appsim.selftest() for line in buf.getvalue().splitlines(): line = line.strip() if line.startswith("[") and "]" in line: status = line[1:line.index("]")].strip() name = line[line.index("]") + 1:].split("|")[0].strip() report.add("selftest", name, status if status in ("PASS", "FAIL", "SKIP", "SENT") else "FAIL") report.add("selftest", "app_sim selftest 退出码", "PASS" if code == 0 else "FAIL", f"exit={code}") # 2) pitch 分析器离线自检:喂一条合法/非法 PITCH 行 from .transports.rtt_session import analyze_lines good = ("[PITCH] on ch8(bass) 52->40 d=-12 deg=3 chord=8 xp=2 xf=0 fl=0x03 FIX\n" "[PITCH] on ch1(chord) 64->64 d=0 deg=1 chord=1 xp=0 xf=0 fl=0x00 PASS\n") res = analyze_lines(good.splitlines()) report.add("selftest", "pitch 分析器(合法样本)", "PASS" if res.fail == 0 and res.ok >= 2 else "FAIL", f"ok={res.ok} fail={res.fail}") bad = "[PITCH] on ch8(bass) 52->30 d=-22 deg=3 chord=8 xp=2 xf=0 fl=0x03 FIX\n" res2 = analyze_lines(bad.splitlines()) report.add("selftest", "pitch 分析器(非法样本检出)", "PASS" if res2.fail >= 1 else "FAIL", f"fail={res2.fail}") # 3) 报告落盘自检 out = os.path.join(default_report_dir(), "k1_selftest.md") report.write_markdown(out) report.print_summary() return report.exit_code() def _run_suites(args) -> int: names = [] for part in args.suite.split(","): part = part.strip().lower() if part == "all": names.extend(n for n in ALL_ORDER if n not in names) elif part in SUITES: if part not in names: names.append(part) else: print(f"未知套件: {part}(可选: {', '.join(SUITES)} / all)", file=sys.stderr) return 2 ctx = Ctx( transport=args.transport, port=args.port or "", baud=args.baud, device=args.device, allow_poweroff=args.allow_poweroff or args.profile == "destructive", dump_png=args.dump_png or "", ) report = HarnessReport(title="K1 HIL 自动化测试报告") report.notes.append(f"套件: {', '.join(names)}") report.notes.append(f"transport={ctx.transport} device={ctx.device} profile={args.profile}") for name in names: mod, needs = SUITES[name] if "transport" in needs and ctx.transport == "com" and not ctx.port: report.add(name, "前置条件", "SKIP", "transport=com 需 --port;已跳过该套件") continue print(f"\n===== 套件 {name} =====", flush=True) try: mod.run(report, ctx) except SystemExit as exc: report.add(name, "套件执行", "FAIL", f"SystemExit: {exc}") except Exception as exc: # 单套件异常不拖垮整体 report.add(name, "套件执行", "FAIL", f"{type(exc).__name__}: {exc}") report.print_summary() ts = report.started.strftime("%Y%m%d_%H%M%S") out = args.report or os.path.join(default_report_dir(), f"k1_hil_{ts}.md") report.write_markdown(out) return report.exit_code() def main() -> None: ap = argparse.ArgumentParser(prog="k1_harness", description="K1 吉他统一 HIL 自动化测试") sub = ap.add_subparsers(dest="cmd", required=True) sub.add_parser("selftest", help="无硬件自检(帧编解码 + 分析器 + 报告)") runp = sub.add_parser("run", help="跑测试套件") runp.add_argument("--suite", required=True, help="逗号分隔: app,ble,keys,keys1617,packs,ui 或 all") runp.add_argument("--transport", choices=("com", "ble"), default="ble", help="app 套件通道(默认 ble)") runp.add_argument("--port", default="", help="COM 端口(transport=com 时必填)") runp.add_argument("--baud", type=int, default=115200) runp.add_argument("--device", default="AT32F403AC", help="J-Link 器件名") runp.add_argument("--profile", choices=("smoke", "nightly", "destructive"), default="nightly") runp.add_argument("--allow-poweroff", action="store_true", help="允许 05 00 关机用例") runp.add_argument("--dump-png", default="", help="ui 套件截屏输出路径(需 DEBUG_LCD_DUMP 固件)") runp.add_argument("--report", default="", help="Markdown 报告输出路径") args = ap.parse_args() if args.cmd == "selftest": raise SystemExit(_run_selftest()) raise SystemExit(_run_suites(args))