Files
STM32F4-Base/test/host/run_boot_test.py
T

502 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""run_boot_test.py - 一键开机日志抓取 + 校验(纯标准库,2026-09-20 替代 run_boot_test.ps1)。
调用仓库内置串口助手(tools/serial_tool,常驻 daemon,子进程 CLI + JSON):
1. open 串口 + clear 日志(本次运行独立代际)
2. 烧录(默认 MDK-ARM\\download.bat:下载 + Reset & Run)
2.5 固件身份戳:从刚烧进去的 .hex 里抠出 `fw build: <DATE> <TIME>`,追加成 Required
标记——芯片里跑的必须就是这份构建(--no-fw-check 关闭)
3. wait-idle:RX 静默 >= settle-ms 收尾;首字节超过 --first-wait-ms 未到 → no_data
(快速失败);整体 max-wait-ms 超时
4. 从日志提取 [RX] 载荷行(剥掉 "[RX] <ISO时间戳> " 前缀),并给每行加 `[+Nms]` 相对
偏移(daemon 毫秒时基,本次第一行 = +0ms;--raw-log 出纯载荷)。MCU 侧时间戳只有
秒级、开机头几行还没时钟,量不了耗时——这个前缀就是耗时口径
4.5 烧录回读校验:MDK-ARM\\reset.bat cmp(= ST-LINK_CLI -CmpFile 全片逐字节比对,
`No difference found` 才算真写进去;--no-verify-flash 关闭)。
**必须排在采集之后**:实测 -CmpFile 连接时会复位目标(板子当场重打一遍开机日志)
5. verify_log.py 标记校验(含启动锚点计数:`reset cause:` 出现 0 次或 >N 次 = FAIL,
N 由 --max-boot-count 给,默认 1;--expect-reset-cause 可对第一个锚点做原因白名单;
--require-order 查标记先后顺序;--done-marker + --max-boot-ms 量开机耗时)
6. (仅当 --require-selftest-pass)自检记录联合判定(S7):
--selftest-log 提供本地副本则直接用它,否则从设备 FTP 抓取 /log/selftest.log
(lftpd :21 匿名);判定:文件缺失 / 0 字节 = 无故障 → PASS,有 64B 记录 = FAIL
采集在烧录**之前**武装(open + clear 先于烧录),daemon 常驻全量日志兜底,
无采集窗口/首字节等待仪式(与旧 PowerShell 版行为一致)。
相对旧 PowerShell 版的加固(本机 Windows 环境实测教训,2026-09-20):
- 子进程输出按**字节**读取、显式 UTF-8 解码 —— 不受控制台代码页(GBK)影响
- serial_config.json 按 UTF-8(容忍 BOM)解析 —— 无 PS 5.1 按 GBK 读 JSON
导致 "Unterminated string" 崩溃的问题
- 不经 PowerShell —— 无执行策略(scripts disabled)拦截
- 烧录带超时(默认 180s;旧版无超时,坏 SWD 连接会永久挂起)
用法:
& '.venv\\Scripts\\python.exe' test/host/run_boot_test.py
[--port COM7] [--baud-rate 115200] [--settle-ms 8000] [--max-wait-ms 30000]
[--first-wait-ms 12000]
[--flash-cmd MDK-ARM\\download.bat] [--flash-timeout 180]
[--required "App init OK;Network Init;net init: OK"]
[--forbidden "FAIL;ERROR;HardFault;[ERR]"]
[--require-suite-summary "Storage Tests"]
[--require-selftest-pass] [--selftest-log <本地副本>]
[--device-ip 192.168.1.100] [--out-dir <目录>] [--show-tail-lines 15]
[--fw-hex <构建产物 hex>] [--no-fw-check]
[--no-verify-flash] [--readback-timeout 120]
[--max-boot-count 1] [--expect-reset-cause 'software;power-on']
[--require-order 'App init OK>Network Init>net init: OK']
[--done-marker 'net init: OK'] [--max-boot-ms 0]
[--raw-log] [--keep-out 20]
--port 缺省取 .serialtest/config.json 的 portName(单一配置源)
退出码(与旧版一致):
0 = PASS(采集成功 + Required 全中 + Forbidden 零命中)
1 = FAIL(标记缺失/命中)
2 = 采集或烧录错误(含烧录回读不一致 = 芯片内容与刚编的镜像不同,本次运行作废)
3 = 用法/预检错误
"""
import argparse
import ftplib
import json
import shlex
import subprocess
import sys
import tempfile
import time
from datetime import datetime
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
# 本脚本位于 test/host/,仓库根需上溯两级(2026-10 实机窗口修:原 .parent 只到 test/,
# 导致 .serialtest/config.json 与 tools/serial_tool 路径解析错误)
REPO_ROOT = SCRIPT_DIR.parent.parent
SERIAL_TOOL = REPO_ROOT / "tools" / "serial_tool" / "serial_tool.py"
CFG_FILE = REPO_ROOT / ".serialtest" / "config.json"
DEFAULT_FLASH = REPO_ROOT / "MDK-ARM" / "download.bat"
RESET_BAT = REPO_ROOT / "MDK-ARM" / "reset.bat"
DEFAULT_HEX = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.hex"
REQUIRED_DEFAULT = "App init OK;Network Init;net init: OK"
FORBIDDEN_DEFAULT = "FAIL;ERROR;HardFault;[ERR]"
# 固件身份戳(App/app/app_main.c 的 `DBG_INFO("fw build: " __DATE__ " " __TIME__)`)。
# 编译期展开成一段连续 ASCII,故能直接从 .hex 里抠出来当 Required 标记用:
# 芯片里跑的镜像 == 刚编出来的镜像,这才叫"烧进去了"。此前所有标记都是固定串,
# 旧镜像照样全中——AGENTS.md 烧录铁律记的"反复重下载、芯片一直是旧镜像"在
# 自动闭环里完全不可见(2026-10-10 加)。
FW_MARKER = "fw build: "
def _split_cmd(cmd_str):
r"""把 `--flash-cmd` 这类命令串拆成 [exe, arg, ...](2026-10-10 实测坑)。
直接交给 subprocess、**不经 `cmd /c`**:CreateProcess 能直接跑 .bat,且 Python
会给含空格的路径正确加引号。旧写法 `['cmd','/c', 串]` 在本仓库(目录名
`DTU Code` 含空格)会被 cmd 的 /c 引号规则玩坏——实测报
`'D:\Code\DTU' is not recognized`,回读校验被假判成"芯片内容不一致"。
路径本身含空格时把该段用引号包住(`"D:\some dir\reset.bat" flash`)。
posix=False 是必须的:posix=True 会吃掉 Windows 路径里的反斜杠。
"""
return [p.strip('"') for p in shlex.split(cmd_str, posix=False)]
def _run_cmd(argv, timeout):
"""跑一条外部命令,按字节取回输出并显式 UTF-8 解码(不受控制台代码页影响)。"""
try:
proc = subprocess.run(argv, capture_output=True, timeout=timeout)
rc = proc.returncode
raw = (proc.stdout or b"") + (proc.stderr or b"")
except subprocess.TimeoutExpired as e:
rc = -1
raw = (e.stdout or b"") + (e.stderr or b"")
return rc, raw.decode("utf-8", errors="replace")
def extract_fw_stamp(hex_path):
"""从 Intel HEX 里抠固件身份戳(只扫数据记录,不建整片镜像,内存 O(镜像体积))。
返回戳串(如 'Oct 10 2026 02:38:12');找不到(旧固件/未重编译)返回 None。
"""
data = bytearray()
for line in Path(hex_path).read_text(errors="replace").splitlines():
line = line.strip()
if not line.startswith(":"):
continue
try:
rec = bytes.fromhex(line[1:])
except ValueError:
continue
if len(rec) > 4 and rec[3] == 0:
data += rec[4:4 + rec[0]]
i = data.find(FW_MARKER.encode("ascii"))
if i < 0:
return None
j = i + len(FW_MARKER)
end = j
while end < len(data) and 0x20 <= data[end] < 0x7F:
end += 1
return data[j:end].decode("ascii", errors="replace")
def _cli(cmd_args, timeout):
"""调用串口助手 CLI,返回 (rc, stdout_text, parsed_json_or_None)。
全程字节级处理(capture_output 取 bytes 后显式 UTF-8 解码),
任何 Windows 控制台代码页都不参与数据路径。
"""
try:
proc = subprocess.run(cmd_args, capture_output=True, timeout=timeout)
except subprocess.TimeoutExpired:
return 2, "client timeout ({}s)".format(timeout), None
text = proc.stdout.decode("utf-8", errors="replace")
try:
return proc.returncode, text, json.loads(text)
except ValueError:
return proc.returncode, text, None
def _tool_cmd(py, cmd, *rest, timeout=15):
rc, out, data = _cli([py, str(SERIAL_TOOL), cmd, *rest], timeout)
if rc != 0 or data is None:
return None, out
return data, out
def _ftp_quit(ftp):
"""尽力关闭 FTP 控制连接(QUIT 失败不影响判定)。"""
try:
ftp.quit()
except (OSError, ftplib.Error):
pass
def _prune_out_dir(out_dir, keep):
"""只保留最近 N 份产物(`boot_log_*.txt` / `selftest_log_*.log`)。
这些文件按 `%Y%m%d_%H%M%S` 命名、每次 @test 各留一份,旧实现从不清理——
本机实测 out 目录已堆到 31 份 boot_log + 7 份 selftest_log(2026-10-10)。
文件名含时间戳 → 按名字排序即按时间排序。
"""
if keep <= 0:
return 0
removed = 0
for pat in ("boot_log_*.txt", "selftest_log_*.log"):
for p in sorted(out_dir.glob(pat), key=lambda q: q.name)[:-keep]:
try:
p.unlink()
removed += 1
except OSError:
pass
return removed
def _fetch_selftest_log(device_ip, out_dir, stamp):
"""从设备 FTP 抓取 /log/selftest.log(lftpd :21 匿名,根目录 = 卷根)。
成功返回本地路径字符串;失败(网络不可达 / FTP 错误 / 设备侧文件缺失)返回 ""
(按 S7 语义视为无故障记录;此处打 WARN,调用方不判整体 FAIL)。
2026-10-02 台架修复:原实现只 `except OSError`,而 ftplib 的 error_temp /
error_perm 继承自 Exception 而非 OSError —— 台账"仅故障才写",干净设备上
/log/selftest.log 根本不存在,RETR 返回 450 直接抛出未捕获异常,令整个
`@test --require-selftest-pass` 在最后一步崩掉(而它本该是 PASS 路径)。
现把"设备侧无此文件"单列为正常路径(INFO),其余 FTP 错误保留 WARN。
"""
local = Path(out_dir) / "selftest_log_{}.log".format(stamp)
ftp = ftplib.FTP()
try:
ftp.connect(device_ip, 21, timeout=10)
ftp.login() # 匿名(lftpd 接受 USER anonymous / 空 PASS)
except (OSError, ftplib.Error) as e:
print("RUN_TEST_WARN: selftest fetch failed: {} "
"(按 S7 语义视为无记录;设备网络/FTP 需另行排查)".format(e))
return ""
buf = bytearray()
try:
ftp.retrbinary("RETR /log/selftest.log", buf.extend)
except ftplib.all_errors as e:
# 450/550 = 设备侧无此文件:干净台账,S7 判 PASS(正常路径,非故障)
print("SELFTEST_FETCH: {} -> (ledger absent: {}) 按 S7 视为无故障记录"
.format(device_ip, str(e).strip()))
_ftp_quit(ftp)
return ""
_ftp_quit(ftp)
local.write_bytes(bytes(buf))
print("SELFTEST_FETCH: {} -> {} ({} bytes)".format(device_ip, local, len(buf)))
return str(local)
def main(argv=None):
# 控制台输出固定 UTF-8:Windows 代码页只影响显示,不会让脚本崩溃
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
ap = argparse.ArgumentParser(description="boot log capture + verify (pure stdlib, no PowerShell)")
ap.add_argument("--port", default="",
help="serial port (default: portName from .serialtest/config.json)")
ap.add_argument("--baud-rate", type=int, default=115200)
ap.add_argument("--settle-ms", type=int, default=8000,
help="RX silence that ends the capture")
ap.add_argument("--max-wait-ms", type=int, default=30000,
help="overall wait-idle timeout")
ap.add_argument("--first-wait-ms", type=int, default=12000,
help="give up as no_data if the board sent ZERO bytes this long "
"after flash started the wait (0 = same as --max-wait-ms)")
ap.add_argument("--flash-cmd", default="",
help="flash command (default: MDK-ARM/download.bat; pass reset.bat for reset-only)")
ap.add_argument("--flash-timeout", type=int, default=180,
help="flash subprocess timeout in seconds")
ap.add_argument("--required", default=REQUIRED_DEFAULT,
help="required markers, ';' separated")
ap.add_argument("--forbidden", default=FORBIDDEN_DEFAULT,
help="forbidden patterns, ';' separated")
ap.add_argument("--require-suite-summary", default="",
help="suite names that MUST appear as '=== <name>: N/M PASSED (failed=0) ===' "
"with N>0, ';' separated (T-2). Empty = off")
ap.add_argument("--require-selftest-pass", action="store_true",
help="jointly judge /log/selftest.log (S7): missing/empty = PASS, "
"any 64B record = FAIL")
ap.add_argument("--selftest-log", default="",
help="local copy of /log/selftest.log (skips device-side fetch when given)")
ap.add_argument("--device-ip", default="192.168.1.100",
help="device IP for fetching /log/selftest.log via FTP (lftpd anonymous)")
ap.add_argument("--out-dir", default=str(Path(tempfile.gettempdir()) / "serial-boot-test"))
ap.add_argument("--show-tail-lines", type=int, default=15)
ap.add_argument("--fw-hex", default=str(DEFAULT_HEX),
help="built image for the firmware-identity stamp and the readback "
"compare (default: latest build hex)")
ap.add_argument("--no-fw-check", action="store_true",
help="do not require the 'fw build: <DATE> <TIME>' stamp in the boot log")
ap.add_argument("--no-verify-flash", action="store_true",
help="skip the ST-LINK -CmpFile readback (AGENTS.md makes it mandatory)")
ap.add_argument("--readback-timeout", type=int, default=120,
help="timeout for the readback compare, seconds")
ap.add_argument("--max-boot-count", type=int, default=1,
help="fail if the boot banner ('reset cause:') appears 0 times or more "
"than N times (0 = check off)")
ap.add_argument("--expect-reset-cause", default="",
help="';'-separated substrings the FIRST reset cause must match "
"(case-insensitive); empty = report only, no verdict")
ap.add_argument("--require-order", default="",
help="'>'-separated markers that must appear in that line order "
"(catches out-of-order boot steps); empty = off")
ap.add_argument("--done-marker", default="net init: OK",
help="marker whose line marks boot completion (for --max-boot-ms); "
"empty = don't measure")
ap.add_argument("--max-boot-ms", type=int, default=0,
help="fail if time from first line to --done-marker exceeds N ms "
"(0 = measure and report only)")
ap.add_argument("--raw-log", action="store_true",
help="write payload-only boot log (no '[+Nms]' relative-time prefix)")
ap.add_argument("--keep-out", type=int, default=20,
help="keep the newest N boot_log_*/selftest_log_* files in --out-dir "
"(0 = keep everything)")
args = ap.parse_args(argv)
# 串口助手 CLI 用当前解释器(AGENTS.md 统一入口 = 仓库 .venv python)
py = sys.executable
# 端口缺省:单一配置源 .serialtest/config.json(UTF-8,容忍 BOM)
port = args.port
if not port and CFG_FILE.is_file():
try:
cfg = json.loads(CFG_FILE.read_text(encoding="utf-8-sig"))
port = str(cfg.get("portName") or "")
except (OSError, ValueError) as e:
print("RUN_TEST_ERROR: cannot parse {}: {}".format(CFG_FILE, e))
return 3
if not port:
print("RUN_TEST_ERROR: no --port given and no portName in {}".format(CFG_FILE))
return 3
# --flash-cmd 允许带参数(如 "MDK-ARM\reset.bat flash"):拆成 argv,
# 存在性只查 exe 段(整串当文件名必然 missing,2026-10-10 实测)
flash_argv = _split_cmd(args.flash_cmd) if args.flash_cmd else [str(DEFAULT_FLASH)]
flash_cmd = Path(flash_argv[0])
# 预检(对应旧版 missing-file 检查)
for p in (SERIAL_TOOL, SCRIPT_DIR / "verify_log.py", flash_cmd):
if not p.is_file():
print("RUN_TEST_ERROR: missing {}".format(p))
return 3
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
pruned = _prune_out_dir(out_dir, args.keep_out)
if pruned:
print("OUT: pruned {} old artifact file(s) in {} (keep={})".format(
pruned, out_dir, args.keep_out))
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
boot_txt = out_dir / "boot_log_{}.txt".format(stamp)
def tool(cmd, *rest, timeout=15):
return _tool_cmd(py, cmd, *rest, timeout=timeout)
print("=== Boot test {} ===".format(stamp))
print("Port: {} @ {} settle={}ms max={}ms".format(
port, args.baud_rate, args.settle_ms, args.max_wait_ms))
# 1. open(幂等;未运行则自动拉起常驻 daemon)
data, out = tool("open", "--port", port, "--baud", str(args.baud_rate), "--timeout", "15")
if data is None:
print("RUN_TEST_ERROR: open failed: {}".format(out))
return 2
# 2. clear(新代际;此后数据才属于本次运行)
data, out = tool("clear", "--timeout", "15")
if data is None:
print("RUN_TEST_ERROR: clear failed: {}".format(out))
return 2
# 3. flash(下载 + Reset & Run)。烧录失败不中断:板子可能已复位,
# 由采集 + 标记判定结果(与旧版一致)
print("FLASH_START: {}".format(" ".join(flash_argv)))
t0 = time.monotonic()
try:
proc = subprocess.run(flash_argv,
capture_output=True, timeout=args.flash_timeout)
flash_rc = proc.returncode
flash_bytes = (proc.stdout or b"") + (proc.stderr or b"")
except subprocess.TimeoutExpired as e:
flash_rc = -1
flash_bytes = (e.stdout or b"") + (e.stderr or b"")
elapsed_ms = int((time.monotonic() - t0) * 1000)
for line in flash_bytes.decode("utf-8", errors="replace").strip().splitlines()[-12:]:
print(line)
print("FLASH_RC: {} elapsed_ms={}".format(flash_rc, elapsed_ms))
if flash_rc != 0:
print("RUN_TEST_WARN: flash rc={} (continuing: board may still have reset)".format(flash_rc))
# 3.5 固件身份戳:从**刚烧进去的那份 .hex** 里抠出 `fw build: <DATE> <TIME>`,
# 追加成 Required 标记 → 芯片里跑的必须就是这份构建。旧 hex 里没有该串
# (未重编译)时只 WARN 跳过,不假判 FAIL。
required = args.required
fw_hex = Path(args.fw_hex)
if args.no_fw_check:
print("FW ID: skipped (--no-fw-check)")
elif not fw_hex.is_file():
print("RUN_TEST_WARN: fw hex not found {} -> identity + readback skipped".format(fw_hex))
else:
fw_stamp = extract_fw_stamp(fw_hex)
if fw_stamp is None:
print("RUN_TEST_WARN: no '{}' stamp in {} -> identity check skipped "
"(old build: run @build first)".format(FW_MARKER, fw_hex.name))
else:
marker = FW_MARKER + fw_stamp
required = (args.required + ";" + marker) if args.required else marker
print("FW ID: expected stamp '{}' (from {})".format(marker, fw_hex.name))
# 4. wait-idle(RX 静默收尾或整体超时)
# --first-timeout-ms:首字节门——烧录后板子全程不吐字节时按 no_data 快速失败
# (旧版 daemon 常驻累计 rx_bytes>0,no_data 永不触发,只能靠后面"日志里没有
# [RX] 行"兜底,报错信息误导成 device silent?)
wait_timeout_s = args.max_wait_ms // 1000 + 20
wait_args = ["--settle-ms", str(args.settle_ms),
"--max-ms", str(args.max_wait_ms),
"--timeout", str(wait_timeout_s)]
if args.first_wait_ms:
wait_args += ["--first-timeout-ms", str(args.first_wait_ms)]
data, out = tool("wait-idle", *wait_args, timeout=wait_timeout_s + 15)
if data is None:
print("RUN_TEST_ERROR: wait-idle failed: {}".format(out))
return 2
print("WAIT: reason={} rx_bytes={} waited_ms={}".format(
data.get("reason"), data.get("rx_bytes"), data.get("waited_ms")))
if data.get("reason") == "no_data":
print("RUN_TEST_ERROR: no RX data at all (check port / board power / flash)")
return 2
# 5. 提取 [RX] 载荷行(剥掉 "[RX] <ISO时间戳> " 前缀)
# 默认给每行加 `[+Nms]` 相对偏移(daemon 毫秒时基,本次采集第一行 = +0ms)。
# 为什么加:MCU 侧时间戳只有**秒级**,且开机头几行时钟还没起(`----/--/--`),
# 根本量不了耗时;有了它 verify_log 才能查"能开机但慢了几秒"(--max-boot-ms)。
# --raw-log 恢复旧格式(纯载荷)。
data, out = tool("log", "--tail", "0", "--timeout", "15")
if data is None:
print("RUN_TEST_ERROR: log read failed: {}".format(out))
return 2
rx = []
t0 = None
for l in data.get("lines", []):
if not (isinstance(l, str) and l.startswith("[RX] ")):
continue
rest = l[5:]
sp = rest.find(" ")
ts_txt = rest[:sp] if sp >= 0 else ""
payload = rest[sp + 1:] if sp >= 0 else rest
if args.raw_log:
rx.append(payload)
continue
ts = None
if ts_txt:
try:
ts = datetime.fromisoformat(ts_txt)
except ValueError:
ts = None
if ts is None:
rx.append(payload) # 时间戳读不出来就不加前缀(不猜)
continue
if t0 is None:
t0 = ts
rx.append("[+{}ms] {}".format(int((ts - t0).total_seconds() * 1000), payload))
if not rx:
print("RUN_TEST_ERROR: no [RX] lines in log (device silent?)")
return 2
boot_txt.write_bytes(("\r\n".join(rx) + "\r\n").encode("utf-8"))
print("Boot log: {} (lines={} gen={} ts={})".format(
boot_txt, len(rx), data.get("gen"), "raw" if args.raw_log else "+ms"))
# 5.6 烧录回读校验(AGENTS.md 铁律:`No difference found` 才算真写进去)。
# 必须排在**采集之后**:实测 -CmpFile 连接时会复位目标(板子当场重打一遍开机
# 日志),排在采集前会截断开机日志并让启动锚点计数误判成"中途复位"。
if args.no_verify_flash:
print("READBACK: skipped (--no-verify-flash)")
elif not fw_hex.is_file():
print("READBACK: skipped (no fw hex: {})".format(fw_hex))
else:
rb_rc, rb_text = _run_cmd(_split_cmd('{} cmp "{}"'.format(RESET_BAT, fw_hex)),
args.readback_timeout)
# 判定按**行首**匹配:进度条和 reset.bat 的帮助文本里都含 "No difference found"
# 字样,只有 ST-LINK 自己那行才独占行首(实测 `...100%\r\nNo difference found.`)。
rb_ok = any(ln.strip().lower().startswith("no difference found")
for ln in rb_text.splitlines())
if rb_ok:
print("READBACK: OK (No difference found) rc={}".format(rb_rc))
else:
print("READBACK: FAIL -- device flash does NOT match {}".format(fw_hex.name))
for line in rb_text.strip().splitlines()[-10:]:
print(" | {}".format(line))
print("RUN_TEST_ERROR: flash readback mismatch (chip content != built image)")
return 2
# 5.5 自检记录抓取(S7):启用且未提供本地副本时才走 FTP
selftest_local = args.selftest_log
if args.require_selftest_pass and not selftest_local:
selftest_local = _fetch_selftest_log(args.device_ip, out_dir, stamp)
# 6. 标记校验(含固件身份戳、启动锚点计数、可选的自检记录联合判定)
from verify_log import verify
rc = verify(str(boot_txt), required, args.forbidden, args.show_tail_lines,
suite_summary=args.require_suite_summary,
selftest_pass=args.require_selftest_pass,
selftest_log=selftest_local,
max_boot_count=args.max_boot_count,
expect_reset_cause=args.expect_reset_cause,
require_order=args.require_order,
done_marker=args.done_marker,
max_boot_ms=args.max_boot_ms)
print("RUN_TEST_RC={}".format(rc))
return rc
if __name__ == "__main__":
sys.exit(main())