4. 回归测试的自动化调度

当回归用例从几十个增长到几百上千个时,手工在终端里逐个敲 xrun 已经不可行。真正需要解决的是三件事:批量提交(一次触发跑完全部用例)、并行执行(把用例分发到多核或多机,压缩墙钟时间)、结果汇总(把分散的日志、退出码、覆盖率收敛成一份可判读的报告)。本节围绕这三件事,给出可直接落地的脚本与 CI 组织方式。

一、回归调度的整体结构

自动化调度的本质是一条“用例清单 → 任务分发 → 并行执行 → 结果回收 → 判定”的流水线。用例清单是唯一事实来源,通常是一个文本文件或 YAML,每行描述一个用例的编译选项、仿真选项和随机种子。调度器读取清单后,把每个用例包装成一个独立任务,交给并行执行层(本地多进程、GNU Parallel、LSF/SGE 集群或 CI 的 job matrix),每个任务在独立目录中运行,避免工作目录互相污染。执行完成后,结果回收层扫描每个用例目录下的退出码与日志,生成汇总表。

Xcelium 回归自动化调度数据流 用例清单 regress.list 用例名 / 顶层 / seed 编译与仿真选项 调度器 run_regress.sh 生成任务 / 分配目录 控制并发度 -j N 并行执行层 xargs -P / GNU Parallel LSF bsub / CI matrix 每任务独立 workdir 结果汇总 退出码 + 日志扫描 summary.csv / HTML 覆盖率合并 并行任务展开(示例:4 路并发) job_001 smoke_test seed=1 PASS job_002 fifo_full seed=7 FAIL job_003 arb_grant seed=3 PASS job_004 reset_seq seed=11 TIMEOUT summary.csv 汇总结果 用例 seed 退出码 状态 耗时(s) smoke_test 1 0 PASS 42 fifo_full 7 1 FAIL 38

二、用例清单与批量提交

清单文件建议采用“一行一用例”的纯文本格式,便于 shell 解析,也便于 CI 中按行切分。字段用制表符或竖线分隔,至少包含用例名、顶层模块、随机种子和额外仿真参数。例如:

# regress.list  用例名|顶层|seed|额外参数
smoke_test|tb_smoke|1|
fifo_full|tb_fifo|7|+UVM_TESTNAME=fifo_full_test
arb_grant|tb_arb|3|+UVM_TESTNAME=arb_grant_test
reset_seq|tb_reset|11|+UVM_TESTNAME=reset_test

批量提交脚本的核心是“读清单、建目录、起任务”。下面是一个可用的骨架,使用 xargs -P 实现本地并行,每个用例在 run/<用例名>_<seed> 目录下执行,日志重定向到该目录的 sim.log,退出码写入 exit.code:

#!/bin/bash
# run_regress.sh  —— 批量提交 Xcelium 回归
LIST=${1:-regress.list}
JOBS=${2:-8}
ROOT=$(pwd)/run
mkdir -p "$ROOT"

run_one() {
  IFS='|' read -r name top seed extra <<< "$1"
  wd="$ROOT/${name}_${seed}"
  mkdir -p "$wd"
  cd "$wd" || exit 99
  xrun -elaborate -access +rwc "$top" \
       -seed "$seed" $extra \
       -l sim.log > /dev/null 2>&1
  echo $? > exit.code
}
export -f run_one
export ROOT

grep -v '^#' "$LIST" | grep -v '^$' \
  | xargs -I{} -P "$JOBS" bash -c 'run_one "$@"' _ {}

这里有两个关键点。第一,-P "$JOBS" 控制并发度,经验值是物理核数的 1~1.5 倍;Xcelium 单实例本身可能多线程,盲目开满反而因内存带宽争抢而变慢。第二,每个任务必须 cd 到独立目录,否则多个 xrun 会争抢同一份 xcelium.d 编译库和波形文件,导致随机失败。

三、并行执行:本地与集群两种形态

本地并行适合几十到一两百个用例、单机多核的场景,优点是零依赖、调试直观。当用例数上千或单用例仿真时间长时,应转向集群调度。以 LSF 为例,把上面的 run_one 封装成 bsub 提交:

bsub -J "reg_${name}_${seed}" \
     -n 1 -R "rusage[mem=4096]" \
     -o "${wd}/lsf.out" \
     "cd ${wd} && xrun -seed ${seed} ${extra} -l sim.log"

在 CI 中(如 GitLab CI 或 Jenkins),更推荐用 job matrix 把用例清单展开成并行 job,每个 job 只跑一个用例,天然隔离且失败可重跑。无论哪种形态,都要保证:任务之间无共享可写资源、每个任务有唯一工作目录、超时时间显式设置(例如 timeout 1800 xrun ...),否则一个挂死的用例会拖垮整轮回归。

四、结果汇总与判定

执行完成后,汇总脚本遍历 run/ 下所有子目录,读取 exit.code 和 sim.log,生成 summary.csv。判定规则要明确:退出码为 0 且日志中无 UVM_ERROR/UVM_FATAL 记为 PASS;退出码非 0 记为 FAIL;被 timeout 杀掉的记为 TIMEOUT;日志中出现 UVM_WARNING 超过阈值可标记为 WARN,供人工复核。

#!/bin/bash
# collect.sh —— 汇总回归结果
echo "case,seed,exit,status,elapsed" > summary.csv
for d in run/*/; do
  name=$(basename "$d")
  code=$(cat "$d/exit.code" 2>/dev/null || echo 99)
  if grep -qE 'UVM_FATAL|UVM_ERROR' "$d/sim.log" 2>/dev/null; then
    status=FAIL
  elif [ "$code" = "124" ]; then
    status=TIMEOUT
  elif [ "$code" = "0" ]; then
    status=PASS
  else
    status=FAIL
  fi
  echo "${name},${code},${status}" >> summary.csv
done
退出码 124 是 GNU timeout 命令在超时终止进程时返回的约定值,把它单独识别为 TIMEOUT,可以避免把“挂死”误判为普通编译错误,便于后续针对性排查。

五、CI 集成与失败处理

把上述脚本接入 CI 时,典型流水线是:checkout → compile(一次编译,生成共享库)→ run_regress.sh(并行跑用例)→ collect.sh(汇总)→ publish(归档 summary.csv 与失败用例日志)。编译与仿真分离能显著节省时间:所有用例复用同一份 xcelium.d,只改变 seed 和 +UVM_TESTNAME。

失败处理上,建议在汇总后自动提取 FAIL 用例清单,生成一个 rerun.list,供开发者一键重跑;同时把失败用例的 sim.log 尾部若干行作为 artifact 上传,避免为了看一条报错而重新拉取整个回归目录。对于偶发失败(同一用例不同 seed 结果不一致),应在汇总表中保留 seed 字段,便于判断是否为随机激励触发的边界问题。

不要在 CI 中把回归脚本写成“任一用例失败即中断整轮”。正确做法是让所有用例跑完再统一判定,否则会丢失大量有用信息,也无法区分“真失败”与“被中断”。