OpenLava EDA 实战(四):大规模回归测试调度与自动化

OpenLava EDA 实战(四):大规模回归测试调度与自动化

摘要:Regression(回归测试)是验证团队最高频的大规模计算场景——几百上千个 testcase 一次性提交,几个小时后看结果。本文讲解如何用 OpenLava 高效组织回归测试:编译与仿真的依赖管理、作业数组批量提交、结果自动汇总、邮件报告、失败用例自动重跑、以及回归平台的整体架构设计。读完你能搭出一套一键提交、自动汇总的回归系统。

前三篇讲了集群搭建、License 调度、VCS 仿真实践。这一篇讲回归测试——EDA 集群最典型的大规模场景。


零、读前必看

0.1 适合谁

  • ✅ 验证团队负责人 / CAD 工程师
  • ✅ 手动跑回归太累,想自动化
  • ✅ 已经在用 OpenLava,想做回归平台

0.2 你将学到

主题 重点
回归工作流 编译 → 仿真 → 汇总 → 报告
作业依赖 -w 实现 DAG 流水线
作业数组 几百个用例一键提交
结果汇总 自动统计 pass/fail,生成报告
失败重跑 失败用例自动带波形重跑
邮件通知 跑完自动发结果邮件
平台化 回归平台架构设计思路

一、回归测试整体流程

1.1 典型流程

┌────────────┐
│  1. 编译    │  vcs compile → 生成 simv
│  (1 个作业)  │
└──────┬─────┘
       │ 编译完成后触发
       ▼
┌────────────────────────────┐
│  2. 批量仿真                  │  N 个 testcase 并行跑
│  (作业数组,100-1000 个作业)  │
└──────────────┬──────────────┘
               │ 所有仿真完成后触发
               ▼
┌────────────┐
│  3. 汇总    │  统计 pass/fail,生成报告
│  (1 个作业)  │
└──────┬─────┘
       │
       ▼
┌────────────┐
│  4. 通知    │  邮件 / 钉钉 / 企业微信
│  (1 个作业)  │
└────────────┘

1.2 关键设计点

设计点 方案
编译 1 次,仿真 N 次 所有 testcase 共用同一个 simv
并行度控制 作业数组 %N 限流,避免打爆 License
依赖管理 -w done(jobid) 串起各阶段
结果汇总 所有仿真完成后统一汇总
失败自动重跑 检测失败,自动带波形重跑一次
可观测 实时看进度、邮件通知

二、目录结构

regress/
├── run_regress.sh          # 一键提交脚本
├── config/
│   ├── testlist.txt        # 测试用例列表
│   └── regress_config.sh   # 配置参数
├── scripts/
│   ├── compile.sh          # 编译脚本
│   ├── simulate.sh         # 单用例仿真脚本
│   ├── collect_results.sh  # 结果汇总脚本
│   ├── rerun_failures.sh   # 失败重跑脚本
│   └── send_report.sh      # 发邮件脚本
├── filelist.f              # 设计文件列表
└── runs/                   # 每次回归的结果
    ├── 20240115_1000/
    │   ├── compile/
    │   ├── sim/
    │   │   ├── test_smoke/
    │   │   ├── test_reset/
    │   │   └── ...
    │   ├── report.html
    │   └── summary.txt
    └── 20240116_1430/

每次回归一个独立目录,按时间戳命名,方便回溯。

三、一键提交脚本

3.1 主脚本:run_regress.sh

#!/bin/bash
# ==============================================
# OpenLava VCS Regression Runner
# Usage: ./run_regress.sh [testlist]
# ==============================================
set -e

# ===== 配置 =====
REGRESS_NAME=${1:-"daily_regression"}
TESTLIST=${2:-"config/testlist.txt"}
VCS_VERSION="O-2018.09-SP2"
QUEUE="vcs_normal"
MAX_PARALLEL=30       # 最多同时跑 30 个
SIM_CORES=1
SIM_MEM=4096          # MB
COMPILE_CORES=8
COMPILE_MEM=16384     # MB

# ===== 目录 =====
RUN_ID=$(date +%Y%m%d_%H%M%S)
RUN_DIR="$(pwd)/runs/${RUN_ID}"
mkdir -p "$RUN_DIR"/{compile,sim,report}

echo "=========================================="
echo " VCS Regression Runner"
echo " Regression Name: $REGRESS_NAME"
echo " Run ID:          $RUN_ID"
echo " Test List:       $TESTLIST"
echo " Max Parallel:    $MAX_PARALLEL"
echo " Run Dir:         $RUN_DIR"
echo "=========================================="

# ===== Step 1: 提交编译作业 =====
echo ""
echo "→ Step 1: Submitting compile job..."

COMPILE_JOB=$(bsub \
  -J "${REGRESS_NAME}_compile" \
  -q "$QUEUE" \
  -n "$COMPILE_CORES" \
  -R "rusage[mem=${COMPILE_MEM}, vcs_license=1] span[hosts=1]" \
  -W 2:00 \
  -o "${RUN_DIR}/compile/compile.log" \
  -e "${RUN_DIR}/compile/compile.err" \
  -env "RUN_DIR=$RUN_DIR VCS_VERSION=$VCS_VERSION" \
  < scripts/compile.sh \
  | awk -F'[<>]' '{print $2}')

echo "  Compile Job ID: $COMPILE_JOB"

# ===== Step 2: 提交仿真作业数组 =====
echo ""
echo "→ Step 2: Submitting simulation job array..."

NUM_TESTS=$(wc -l < "$TESTLIST")
echo "  Number of tests: $NUM_TESTS"

SIM_JOBS=$(bsub \
  -J "${REGRESS_NAME}_sim[1-${NUM_TESTS}]%${MAX_PARALLEL}" \
  -q "$QUEUE" \
  -n "$SIM_CORES" \
  -R "rusage[mem=${SIM_MEM}, vcs_license=1] span[hosts=1]" \
  -W 4:00 \
  -o "${RUN_DIR}/sim/%I.log" \
  -e "${RUN_DIR}/sim/%I.err" \
  -w "done(${COMPILE_JOB})" \
  -env "RUN_DIR=$RUN_DIR TESTLIST=$TESTLIST VCS_VERSION=$VCS_VERSION REGRESS_NAME=$REGRESS_NAME" \
  < scripts/simulate.sh \
  | awk -F'[<>]' '{print $2}')

echo "  Simulation Array Job ID: $SIM_JOBS"

# ===== Step 3: 提交汇总作业 =====
echo ""
echo "→ Step 3: Submitting collect job..."

COLLECT_JOB=$(bsub \
  -J "${REGRESS_NAME}_collect" \
  -q "$QUEUE" \
  -n 1 \
  -R "rusage[mem=2048] span[hosts=1]" \
  -W 30 \
  -o "${RUN_DIR}/report/collect.log" \
  -e "${RUN_DIR}/report/collect.err" \
  -w "done(${SIM_JOBS})" \
  -env "RUN_DIR=$RUN_DIR TESTLIST=$TESTLIST REGRESS_NAME=$REGRESS_NAME" \
  < scripts/collect_results.sh \
  | awk -F'[<>]' '{print $2}')

echo "  Collect Job ID: $COLLECT_JOB"

# ===== Step 4: 提交通知作业 =====
echo ""
echo "→ Step 4: Submitting notify job..."

NOTIFY_JOB=$(bsub \
  -J "${REGRESS_NAME}_notify" \
  -q "$QUEUE" \
  -n 1 \
  -R "rusage[mem=512] span[hosts=1]" \
  -W 10 \
  -o "${RUN_DIR}/report/notify.log" \
  -w "done(${COLLECT_JOB})" \
  -env "RUN_DIR=$RUN_DIR REGRESS_NAME=$REGRESS_NAME" \
  < scripts/send_report.sh \
  | awk -F'[<>]' '{print $2}')

echo "  Notify Job ID: $NOTIFY_JOB"

# ===== 输出 =====
echo ""
echo "=========================================="
echo " Regression submitted successfully!"
echo " Run ID: $RUN_ID"
echo ""
echo " Job Chain:"
echo "   Compile:    $COMPILE_JOB"
echo "   Sim Array:  $SIM_JOBS ($NUM_TESTS tests, max $MAX_PARALLEL parallel)"
echo "   Collect:    $COLLECT_JOB"
echo "   Notify:     $NOTIFY_JOB"
echo ""
echo " Monitor with:"
echo "   bjobs -J \"${REGRESS_NAME}_*\""
echo "   cd $RUN_DIR"
echo "=========================================="

# 保存 run 信息
cat > "${RUN_DIR}/run_info.txt" << EOF
REGRESS_NAME=$REGRESS_NAME
RUN_ID=$RUN_ID
TESTLIST=$TESTLIST
NUM_TESTS=$NUM_TESTS
MAX_PARALLEL=$MAX_PARALLEL
COMPILE_JOB=$COMPILE_JOB
SIM_JOBS=$SIM_JOBS
COLLECT_JOB=$COLLECT_JOB
NOTIFY_JOB=$NOTIFY_JOB
SUBMIT_TIME=$(date)
SUBMITTED_BY=$(whoami)
EOF

3.2 配置文件

# config/regress_config.sh
VCS_VERSION="O-2018.09-SP2"
VERDI_VERSION="Verdi_O-2018.09-SP2"
QUEUE="vcs_normal"
MAX_PARALLEL=30
SIM_MEM=4096
COMPILE_MEM=16384
COMPILE_CORES=8
NOTIFY_EMAIL="verif-team@example.com"
LICENSE_FEATURE="VCS_MX"

3.3 测试用例列表

# config/testlist.txt
test_smoke
test_reset
test_intr_basic
test_intr_level
test_intr_edge
test_dma_single
test_dma_burst
test_dma_scatter_gather
test_pcie_cfg_rw
test_pcie_mem_rw
test_pcie_msi
test_error_correction
test_phy_init
test_phy_bist
# ... 更多用例

四、各阶段脚本

4.1 编译脚本

#!/bin/bash
# scripts/compile.sh

source /etc/profile.d/modules.sh
module load synopsys/scl/2019.06
module load synopsys/vcs/$VCS_VERSION
module load synopsys/verdi/$VERDI_VERSION

echo "========================================"
echo " VCS Compilation"
echo " Run ID: $RUN_ID"
echo " Job ID: $LSB_JOBID"
echo " Host:   $(hostname)"
echo " Start:  $(date)"
echo "========================================"

cd "$RUN_DIR/compile"

# 复制必要文件(假设项目根目录在上两级)
PROJECT_ROOT=$(dirname "$(dirname "$(dirname "$RUN_DIR")")")
cp "$PROJECT_ROOT/filelist.f" .
# 或者直接用绝对路径

# VCS 编译
vcs -full64 \
    -f filelist.f \
    -top tb_top \
    -debug_access+pp \
    -o simv \
    -j$LSB_DJOB_NUMPROC \
    -ntb_opts uvm-1.2 \
    -sverilog \
    -cm line+cond+fsm \
    -l compile.log

EXIT=$?

echo "========================================"
echo " Compile exit code: $EXIT"
echo " End: $(date)"
echo "========================================"

# 编译失败,直接退出(后续作业也会因为 exit 而不跑)
if [ $EXIT -ne 0 ]; then
    echo "COMPILE FAILED"
    exit 1
fi

echo "COMPILE SUCCESS"
exit 0

4.2 仿真脚本

#!/bin/bash
# scripts/simulate.sh

source /etc/profile.d/modules.sh
module load synopsys/scl/2019.06
module load synopsys/vcs/$VCS_VERSION

echo "========================================"
echo " Simulation - Index: $LSB_JOBINDEX"
echo " Run ID: $RUN_ID"
echo " Job ID: $LSB_JOBID"
echo " Host:   $(hostname)"
echo " Start:  $(date)"
echo "========================================"

# 读取 testcase 名
TESTCASE=$(sed -n "${LSB_JOBINDEX}p" "$TESTLIST")
echo "TestCase: $TESTCASE"

if [ -z "$TESTCASE" ]; then
    echo "ERROR: Test case not found for index $LSB_JOBINDEX"
    exit 1
fi

# 结果目录
RESULT_DIR="$RUN_DIR/sim/$TESTCASE"
mkdir -p "$RESULT_DIR"

# 在本地 /tmp 跑(性能好)
WORK_DIR="/tmp/${REGRESS_NAME}_${LSB_JOBID}_${LSB_JOBINDEX}"
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"

# 链接 simv
ln -s "$RUN_DIR/compile/simv" .
ln -s "$RUN_DIR/compile/simv.daidir" . 2>/dev/null || true

# 跑仿真
./simv \
    +UVM_TESTNAME=$TESTCASE \
    +UVM_VERBOSITY=LOW \
    +UVM_TIMEOUT=500000000ns \
    +ntb_random_seed=$LSB_JOBID \
    -cm line+cond+fsm \
    -cm_name ${TESTCASE} \
    -cm_dir "$RUN_DIR/compile/cov.vdb" \
    -l sim.log

EXIT=$?

# 判断结果
RESULT="UNKNOWN"
if grep -q "UVM_ERROR :    0" sim.log && \
   grep -q "UVM_FATAL :    0" sim.log && \
   [ $EXIT -eq 0 ]; then
    RESULT="PASS"
else
    RESULT="FAIL"
fi

echo ""
echo "Result: $RESULT"
echo "End: $(date)"

# 归档结果
cp sim.log "$RESULT_DIR/"
echo "$RESULT" > "$RESULT_DIR/result.txt"
echo "$EXIT" > "$RESULT_DIR/exit_code.txt"

# 如果失败,带波形重跑一次(在后台不阻塞)
if [ "$RESULT" = "FAIL" ]; then
    echo "FAIL - will re-run with waveform in separate job"
    # 提交重跑作业(依赖当前作业完成)
    bsub -J "${REGRESS_NAME}_rerun_${TESTCASE}" \
         -q vcs_normal \
         -n 1 \
         -R "rusage[mem=8192, vcs_license=1] span[hosts=1]" \
         -W 4:00 \
         -o "${RESULT_DIR}/rerun.log" \
         -e "${RESULT_DIR}/rerun.err" \
         -env "RUN_DIR=$RUN_DIR TESTCASE=$TESTCASE VCS_VERSION=$VCS_VERSION RESULT_DIR=$RESULT_DIR REGRESS_NAME=$REGRESS_NAME" \
         < scripts/rerun_waveform.sh
fi

# 清理临时目录
rm -rf "$WORK_DIR"

exit 0

4.3 重跑脚本(失败带波形)

#!/bin/bash
# scripts/rerun_waveform.sh

source /etc/profile.d/modules.sh
module load synopsys/scl/2019.06
module load synopsys/vcs/$VCS_VERSION
module load synopsys/verdi/$VERDI_VERSION

echo "Re-running $TESTCASE with waveform..."
echo "Job ID: $LSB_JOBID, Host: $(hostname)"

WORK_DIR="/tmp/rerun_${LSB_JOBID}"
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"

ln -s "$RUN_DIR/compile/simv" .
ln -s "$RUN_DIR/compile/simv.daidir" . 2>/dev/null || true

./simv \
    +DUMP_FSDB \
    +UVM_TESTNAME=$TESTCASE \
    +UVM_VERBOSITY=DEBUG \
    +UVM_TIMEOUT=500000000ns \
    +ntb_random_seed=$(cat "$RESULT_DIR/exit_code.txt" | head -1 || echo 1) \
    -l sim_debug.log

EXIT=$?

# 归档波形和调试日志
cp sim_debug.log "$RESULT_DIR/"
if [ -f wave.fsdb ]; then
    mv wave.fsdb "$RESULT_DIR/"
    echo "Waveform saved: $RESULT_DIR/wave.fsdb"
fi

rm -rf "$WORK_DIR"
echo "Rerun done, exit=$EXIT"

4.4 结果汇总脚本

#!/bin/bash
# scripts/collect_results.sh

echo "========================================"
echo " Collecting Results"
echo " Run ID: $RUN_ID"
echo " Start:  $(date)"
echo "========================================"

SIM_DIR="$RUN_DIR/sim"
REPORT_DIR="$RUN_DIR/report"
mkdir -p "$REPORT_DIR"

PASS_COUNT=0
FAIL_COUNT=0
SKIP_COUNT=0
TOTAL=0
PASS_TESTS=""
FAIL_TESTS=""

# 遍历所有用例
while IFS= read -r testcase; do
    [ -z "$testcase" ] && continue
    TOTAL=$((TOTAL + 1))

    RESULT_FILE="$SIM_DIR/$testcase/result.txt"
    if [ -f "$RESULT_FILE" ]; then
        RESULT=$(cat "$RESULT_FILE")
        if [ "$RESULT" = "PASS" ]; then
            PASS_COUNT=$((PASS_COUNT + 1))
            PASS_TESTS="$PASS_TESTS $testcase"
        else
            FAIL_COUNT=$((FAIL_COUNT + 1))
            FAIL_TESTS="$FAIL_TESTS $testcase"
        fi
    else
        SKIP_COUNT=$((SKIP_COUNT + 1))
        echo "SKIP (no result): $testcase"
    fi
done < "$TESTLIST"

# 计算通过率
if [ $TOTAL -gt 0 ]; then
    PASS_RATE=$((PASS_COUNT * 100 / TOTAL))
else
    PASS_RATE=0
fi

# ===== 生成文本报告 =====
cat > "$REPORT_DIR/summary.txt" << EOF
========================================
  VCS Regression Report
  Regression: $REGRESS_NAME
  Run ID:     $RUN_ID
  Generated:  $(date)
========================================

Total:    $TOTAL
Passed:   $PASS_COUNT
Failed:   $FAIL_COUNT
Skipped:  $SKIP_COUNT
Pass Rate: ${PASS_RATE}%

========================================
Failed Tests:
========================================
$(for t in $FAIL_TESTS; do echo "  ✗ $t"; done)

========================================
Passed Tests:
========================================
$(for t in $PASS_TESTS; do echo "  ✓ $t"; done)

========================================
EOF

cat "$REPORT_DIR/summary.txt"

# ===== 生成 HTML 报告 =====
generate_html_report() {
    cat > "$REPORT_DIR/report.html" << HTMLEOF
<!DOCTYPE html>
<html>
<head>
    <title>Regression Report - $REGRESS_NAME</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .pass { color: green; font-weight: bold; }
        .fail { color: red; font-weight: bold; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #4CAF50; color: white; }
        tr:nth-child(even) { background-color: #f2f2f2; }
        .summary-box { padding: 15px; background: #e8f5e9; border-radius: 5px; margin-bottom: 20px; }
    </style>
</head>
<body>
    <h1>Regression Report: $REGRESS_NAME</h1>
    <p>Run ID: $RUN_ID | Generated: $(date)</p>

    <div class="summary-box">
        <h2>Summary</h2>
        <p><strong>Total:</strong> $TOTAL</p>
        <p class="pass"><strong>Passed:</strong> $PASS_COUNT</p>
        <p class="fail"><strong>Failed:</strong> $FAIL_COUNT</p>
        <p><strong>Pass Rate:</strong> ${PASS_RATE}%</p>
    </div>

    <h2>Test Results</h2>
    <table>
        <tr><th>Test Case</th><th>Result</th><th>Log</th></tr>
HTMLEOF

    while IFS= read -r testcase; do
        [ -z "$testcase" ] && continue
        RESULT_FILE="$SIM_DIR/$testcase/result.txt"
        if [ -f "$RESULT_FILE" ]; then
            RESULT=$(cat "$RESULT_FILE")
            if [ "$RESULT" = "PASS" ]; then
                CLASS="pass"
            else
                CLASS="fail"
            fi
            echo "<tr><td>$testcase</td><td class=\"$CLASS\">$RESULT</td><td><a href=\"../sim/$testcase/sim.log\">log</a></td></tr>"
        else
            echo "<tr><td>$testcase</td><td>SKIP</td><td>-</td></tr>"
        fi
    done < "$TESTLIST"

    cat >> "$REPORT_DIR/report.html" << HTMLEOF
    </table>
</body>
</html>
HTMLEOF
}

generate_html_report

echo ""
echo "Report generated: $REPORT_DIR/report.html"
echo "Summary: $REPORT_DIR/summary.txt"

exit 0

4.5 邮件通知脚本

#!/bin/bash
# scripts/send_report.sh

echo "Sending regression report email..."

REPORT_DIR="$RUN_DIR/report"
SUMMARY=$(cat "$REPORT_DIR/summary.txt")

# 提取 pass rate
PASS_RATE=$(grep "Pass Rate" "$SUMMARY" | grep -oP '\d+%')
PASS_COUNT=$(grep "Passed" "$SUMMARY" | grep -oP '\d+')
FAIL_COUNT=$(grep "Failed" "$SUMMARY" | grep -oP '\d+')

if [ "$FAIL_COUNT" -eq 0 ]; then
    SUBJECT="[PASS] Regression: $REGRESS_NAME - ${PASS_RATE} pass rate"
else
    SUBJECT="[FAIL] Regression: $REGRESS_NAME - $FAIL_COUNT failed (${PASS_RATE} pass)"
fi

# 发邮件(需要配置 mailx 或 sendmail)
echo "$SUMMARY" | mailx \
    -s "$SUBJECT" \
    -a "$REPORT_DIR/report.html" \
    verif-team@example.com

# 或者用 Python 发(更可靠)
python3 << PYEOF
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

msg = MIMEMultipart()
msg['Subject'] = "$SUBJECT"
msg['From'] = 'eda-cluster@example.com'
msg['To'] = 'verif-team@example.com'

with open('$REPORT_DIR/summary.txt') as f:
    body = f.read()
msg.attach(MIMEText(body, 'plain'))

with open('$REPORT_DIR/report.html') as f:
    msg.attach(MIMEText(f.read(), 'html'))

with smtplib.SMTP('smtp.example.com', 25) as s:
    s.send_message(msg)

print("Email sent successfully")
PYEOF

echo "Notification sent"

五、实时监控回归进度

5.1 看进度命令

# 看整个回归的所有作业
bjobs -J "regress_name_*"

# 看仿真数组进度
bjobs -J "regress_name_sim*"

# 统计各状态数量
bjobs -J "regress_name_sim*" 2>/dev/null | awk 'NR>1 {print $3}' | sort | uniq -c
#    5 DONE
#   15 RUN
#   80 PEND

5.2 进度监控脚本

#!/bin/bash
# scripts/monitor_progress.sh
# Usage: ./monitor_progress.sh <run_dir>

RUN_DIR=$1

if [ -z "$RUN_DIR" ]; then
    echo "Usage: $0 <run_dir>"
    exit 1
fi

SIM_DIR="$RUN_DIR/sim"
TESTLIST="$RUN_DIR/../config/testlist.txt"  # 可能需要调整

if [ ! -f "$RUN_DIR/run_info.txt" ]; then
    echo "Invalid run dir: $RUN_DIR"
    exit 1
fi

source "$RUN_DIR/run_info.txt"

while true; do
    clear
    echo "========================================"
    echo " Regression Monitor: $REGRESS_NAME"
    echo " Run ID: $RUN_ID"
    echo " Time: $(date)"
    echo "========================================"

    # 统计结果
    TOTAL=$NUM_TESTS
    DONE=0
    PASS=0
    FAIL=0

    for d in "$SIM_DIR"/*/; do
        [ -d "$d" ] || continue
        if [ -f "$d/result.txt" ]; then
            DONE=$((DONE + 1))
            if [ "$(cat "$d/result.txt")" = "PASS" ]; then
                PASS=$((PASS + 1))
            else
                FAIL=$((FAIL + 1))
            fi
        fi
    done

    RUNNING=$(( $(bjobs -J "${REGRESS_NAME}_sim*" 2>/dev/null | grep -c RUN || echo 0) ))
    PENDING=$(( $(bjobs -J "${REGRESS_NAME}_sim*" 2>/dev/null | grep -c PEND || echo 0) ))

    echo ""
    echo "  Total:    $TOTAL"
    echo "  Done:     $DONE"
    echo "  Running:  $RUNNING"
    echo "  Pending:  $PENDING"
    echo ""
    echo "  Pass:     $PASS  ✅"
    echo "  Fail:     $FAIL  ❌"
    if [ $DONE -gt 0 ]; then
        echo "  Pass Rate: $((PASS * 100 / DONE))%"
    fi

    # 进度条
    echo ""
    PROGRESS=$((DONE * 50 / TOTAL))
    printf "  ["
    for ((i=0; i<PROGRESS; i++)); do printf "="; done
    printf ">%*s" $((50-PROGRESS)) "" | tr ' ' '.'
    printf "] %d%%\n" $((DONE * 100 / TOTAL))

    echo ""
    echo "  Press Ctrl+C to exit"

    # 如果全部完成了
    if [ $DONE -ge $TOTAL ]; then
        echo ""
        echo "  🎉 Regression complete!"
        break
    fi

    sleep 30
done

六、进阶:回归平台化

6.1 平台架构

Web UI (Python Flask)
    │
    ├── 提交回归(选择 testlist、配置参数)
    ├── 查看进度(实时刷新)
    ├── 查看历史(过去 30 天回归记录)
    ├── 对比回归(两次回归结果对比)
    └── 结果分析(失败趋势、用例波动)
            │
            ▼
    OpenLava (作业调度)
            │
            ▼
    共享存储(结果归档)

6.2 核心功能

功能 说明
一键提交 Web 页面选配置,一键提交回归
实时进度 动态更新的进度条和 pass/fail 数
结果对比 对比两次回归的 pass rate 变化
失败趋势 哪些用例经常失败
覆盖率趋势 覆盖率随时间变化曲线
资源监控 License / CPU / 内存使用情况
用户管理 谁提交了什么,配额管理

6.3 数据持久化

把回归结果存到数据库(SQLite/MySQL):

-- 回归表
CREATE TABLE regressions (
    id INTEGER PRIMARY KEY,
    name VARCHAR(255),
    run_id VARCHAR(50) UNIQUE,
    submit_time DATETIME,
    submitted_by VARCHAR(100),
    total_tests INT,
    pass_count INT,
    fail_count INT,
    pass_rate FLOAT,
    status VARCHAR(20),  -- running, completed, failed
    result_dir VARCHAR(512)
);

-- 用例结果表
CREATE TABLE test_results (
    id INTEGER PRIMARY KEY,
    regression_id INT,
    test_name VARCHAR(255),
    result VARCHAR(10),  -- PASS, FAIL, SKIP
    runtime FLOAT,
    log_path VARCHAR(512),
    FOREIGN KEY (regression_id) REFERENCES regressions(id)
);

七、常见坑与最佳实践

7.1 常见坑

现象 解决
simv 路径不对 仿真作业找不到 simv 用绝对路径,编译完检查文件存在
License 不够 大批作业启动失败 作业数组限流 + License 资源调度
共享存储慢 仿真跑的慢,波形写很久 波形写本地 /tmp,跑完归档
节点环境不一致 某些节点跑失败 用 Module 统一环境,节点上线前全量测试
作业数组 ID 混淆 用名字查找不准 保存 run_info.txt,存所有 job id
编译失败还继续跑 仿真作业全失败 done() 依赖,编译失败仿真不触发

7.2 最佳实践

  • 一键提交:一个脚本搞定所有事
  • 每次回归独立目录:按时间戳命名,互不干扰
  • 编译和仿真分离:编一次跑 N 次
  • 失败自动重跑 + 抓波形:省得手动调
  • 自动汇总 + 邮件:不用手动统计
  • 限流:别一下提交 1000 个全跑起来
  • 实时监控:不用反复敲 bjobs
  • 历史记录:存数据库,方便趋势分析

八、写在最后

回归测试是 EDA 集群最核心的应用场景。把这套流程搭起来,验证团队的效率能上一个大台阶——以前手动提交、手动统计、手动看结果,现在一键提交、自动报告、邮件通知。

核心思路就是"自动化一切能自动化的":

  • 提交自动化(脚本一键搞定)
  • 调度自动化(OpenLava 管资源)
  • 结果自动化(自动汇总 + 发邮件)
  • 调试自动化(失败自动抓波形)

做到这几点,回归就不再是痛苦的事了。


OpenLava EDA 实战系列到此结束。 四篇文章覆盖了从集群搭建、License 调度、VCS 仿真到大规模回归的完整链路。

你们的回归一次跑多少个用例?有什么独门技巧? 评论区聊聊。

OpenLava #EDA #VCS #回归测试 #Regression #UVM #自动化 #运维

发表回复

后才能评论