#!/usr/bin/env bash # 批量回归测试脚本:对 test/test_case 下全部 .sy 用例执行 IR 语义验证。 # 用法:./scripts/run_all_tests.sh [--ir | --asm | --both] # # 默认只测 IR(通过 llc + clang 编译运行)。 # --asm 只测汇编(需要 aarch64-linux-gnu-gcc + qemu-aarch64)。 # --both 同时测 IR 和汇编。 set -uo pipefail now_ns() { date +%s%N } format_ns() { local ns=$1 local ms=$((ns / 1000000)) local sec=$((ms / 1000)) local rem_ms=$((ms % 1000)) printf '%d.%03ds' "$sec" "$rem_ms" } mode="ir" if [[ "${1:-}" == "--asm" ]]; then mode="asm" elif [[ "${1:-}" == "--both" ]]; then mode="both" fi SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$ROOT_DIR" compiler="./build/bin/compiler" if [[ ! -x "$compiler" ]]; then echo "❌ 未找到编译器: $compiler" >&2 echo "请先构建:cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j \$(nproc)" >&2 exit 1 fi find_tool() { local name for name in "$@"; do if command -v "$name" >/dev/null 2>&1; then command -v "$name" return 0 fi done return 1 } LLC_CMD=$(find_tool llc llc-20 || true) CLANG_CMD=$(find_tool clang clang-20 || true) total=0 passed=0 failed=0 skipped=0 fail_list=() RUN_LAST_TOTAL_NS=0 RUN_LAST_BREAKDOWN="" batch_start_ns=$(now_ns) run_ir_test() { local sy="$1" local dir dir=$(dirname "$sy") local stem stem=$(basename "$sy" .sy) local out_dir="test/test_result/ir_batch" mkdir -p "$out_dir" local out_file="$out_dir/$stem.ll" local stdin_file="$dir/$stem.in" local expected_file="$dir/$stem.out" local stdout_file="$out_dir/$stem.stdout" local actual_file="$out_dir/$stem.actual.out" local start_ns start_ns=$(now_ns) # 生成 IR local emit_start_ns emit_start_ns=$(now_ns) if ! timeout 30 "$compiler" --emit-ir "$sy" > "$out_file" 2>/dev/null; then RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns $((RUN_LAST_TOTAL_NS)))" echo " [SKIP-IR] $sy (编译器报错或超时)" return 2 fi local emit_ns=$(( $(now_ns) - emit_start_ns )) # 需要 llc + clang if [[ -z "$LLC_CMD" || -z "$CLANG_CMD" ]]; then RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$emit_ns")" echo " [SKIP-IR] $sy (缺少 llc/llc-20 或 clang/clang-20)" return 2 fi local obj="$out_dir/$stem.o" local exe="$out_dir/$stem" local lower_link_start_ns lower_link_start_ns=$(now_ns) if ! "$LLC_CMD" -filetype=obj "$out_file" -o "$obj" 2>/dev/null; then RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$emit_ns")" echo " [SKIP-IR] $sy ($LLC_CMD 编译失败)" return 2 fi if ! "$CLANG_CMD" -no-pie "$obj" sylib/sylib.c -o "$exe" -lm -pthread 2>/dev/null; then RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$emit_ns") lower+link=$(format_ns $(( $(now_ns) - lower_link_start_ns )))" echo " [SKIP-IR] $sy ($CLANG_CMD 链接失败)" return 2 fi local lower_link_ns=$(( $(now_ns) - lower_link_start_ns )) set +e # performance 用例给更长的超时时间 local run_timeout=3000 if [[ "$sy" == *"performance"* ]]; then run_timeout=3000 fi local run_start_ns run_start_ns=$(now_ns) if [[ -f "$stdin_file" ]]; then timeout $run_timeout "$exe" < "$stdin_file" > "$stdout_file" 2>/dev/null else timeout $run_timeout "$exe" > "$stdout_file" 2>/dev/null fi local status=$? set +e local run_ns=$(( $(now_ns) - run_start_ns )) RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$emit_ns") lower+link=$(format_ns "$lower_link_ns") run=$(format_ns "$run_ns")" # timeout 返回 124 表示超时,标记为 SKIP if [[ $status -eq 124 ]]; then echo " [SKIP-IR] $sy (运行超时)" return 2 fi # 组装实际输出 { cat "$stdout_file" if [[ -s "$stdout_file" ]] && (( $(tail -c 1 "$stdout_file" | wc -l) == 0 )); then printf '\n' fi printf '%s\n' "$status" } > "$actual_file" if [[ ! -f "$expected_file" ]]; then echo " [SKIP-IR] $sy (无预期输出)" return 2 fi if diff -q <(sed -e 's/\r$//' -e '$a\\' "$expected_file") \ <(sed -e 's/\r$//' -e '$a\\' "$actual_file") >/dev/null 2>&1; then return 0 else return 1 fi } run_asm_test() { local sy="$1" local dir dir=$(dirname "$sy") local stem stem=$(basename "$sy" .sy) local out_dir="test/test_result/asm_batch" mkdir -p "$out_dir" local asm_file="$out_dir/$stem.s" local stdin_file="$dir/$stem.in" local expected_file="$dir/$stem.out" local stdout_file="$out_dir/$stem.stdout" local actual_file="$out_dir/$stem.actual.out" local exe="$out_dir/$stem" local start_ns start_ns=$(now_ns) # 生成汇编 local emit_start_ns emit_start_ns=$(now_ns) if ! timeout 30 "$compiler" --emit-asm "$sy" > "$asm_file" 2>/dev/null; then RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$RUN_LAST_TOTAL_NS")" echo " [SKIP-ASM] $sy (编译器报错或超时)" return 2 fi local emit_ns=$(( $(now_ns) - emit_start_ns )) if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$emit_ns")" echo " [SKIP-ASM] $sy (缺少 aarch64-linux-gnu-gcc)" return 2 fi local link_start_ns link_start_ns=$(now_ns) if ! timeout 30 aarch64-linux-gnu-gcc "$asm_file" sylib/sylib.c -o "$exe" -static -pthread 2>/dev/null; then RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$emit_ns") asm+link=$(format_ns $(( $(now_ns) - link_start_ns )))" echo " [SKIP-ASM] $sy (汇编/链接失败)" return 2 fi local link_ns=$(( $(now_ns) - link_start_ns )) if ! command -v qemu-aarch64 >/dev/null 2>&1; then RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$emit_ns") asm+link=$(format_ns "$link_ns")" echo " [SKIP-ASM] $sy (缺少 qemu-aarch64)" return 2 fi set +e # performance 用例给更长的超时时间 local run_timeout=30 if [[ "$sy" == *"performance"* ]]; then run_timeout=1000 fi local run_start_ns run_start_ns=$(now_ns) if [[ -f "$stdin_file" ]]; then timeout $run_timeout qemu-aarch64 -L /usr/aarch64-linux-gnu "$exe" < "$stdin_file" > "$stdout_file" 2>/dev/null else timeout $run_timeout qemu-aarch64 -L /usr/aarch64-linux-gnu "$exe" > "$stdout_file" 2>/dev/null fi local status=$? set +e local run_ns=$(( $(now_ns) - run_start_ns )) RUN_LAST_TOTAL_NS=$(( $(now_ns) - start_ns )) RUN_LAST_BREAKDOWN="emit=$(format_ns "$emit_ns") asm+link=$(format_ns "$link_ns") run=$(format_ns "$run_ns")" # timeout 返回 124 表示超时,标记为 SKIP if [[ $status -eq 124 ]]; then echo " [SKIP-ASM] $sy (运行超时)" return 2 fi { cat "$stdout_file" if [[ -s "$stdout_file" ]] && (( $(tail -c 1 "$stdout_file" | wc -l) == 0 )); then printf '\n' fi printf '%s\n' "$status" } > "$actual_file" if [[ ! -f "$expected_file" ]]; then echo " [SKIP-ASM] $sy (无预期输出)" return 2 fi if diff -q <(sed -e 's/\r$//' -e '$a\\' "$expected_file") \ <(sed -e 's/\r$//' -e '$a\\' "$actual_file") >/dev/null 2>&1; then return 0 else return 1 fi } echo "========================================" echo " Lab4 批量回归测试 (mode: $mode)" echo "========================================" echo " LLVM tools: $LLC_CMD / $CLANG_CMD" echo "" # 收集所有测试文件 mapfile -t test_files < <(find test/test_case -name '*.sy' | sort) for sy in "${test_files[@]}"; do total=$((total + 1)) if [[ "$mode" == "ir" || "$mode" == "both" ]]; then run_ir_test "$sy" rc=$? if [[ $rc -eq 0 ]]; then echo " [PASS-IR] $sy ($(format_ns "$RUN_LAST_TOTAL_NS"); $RUN_LAST_BREAKDOWN)" passed=$((passed + 1)) elif [[ $rc -eq 1 ]]; then echo " [FAIL-IR] $sy ($(format_ns "$RUN_LAST_TOTAL_NS"); $RUN_LAST_BREAKDOWN)" failed=$((failed + 1)) fail_list+=("$sy (IR)") else skipped=$((skipped + 1)) fi fi if [[ "$mode" == "asm" || "$mode" == "both" ]]; then run_asm_test "$sy" rc=$? if [[ $rc -eq 0 ]]; then echo " [PASS-ASM] $sy ($(format_ns "$RUN_LAST_TOTAL_NS"); $RUN_LAST_BREAKDOWN)" if [[ "$mode" == "asm" ]]; then passed=$((passed + 1)) fi elif [[ $rc -eq 1 ]]; then echo " [FAIL-ASM] $sy ($(format_ns "$RUN_LAST_TOTAL_NS"); $RUN_LAST_BREAKDOWN)" if [[ "$mode" == "asm" ]]; then failed=$((failed + 1)) fi fail_list+=("$sy (ASM)") else if [[ "$mode" == "asm" ]]; then skipped=$((skipped + 1)) fi fi fi done echo "" echo "========================================" echo " 测试结果汇总" echo "========================================" echo " 总计: $total" echo " 通过: $passed" echo " 失败: $failed" echo " 跳过: $skipped" echo " 总耗时: $(format_ns $(( $(now_ns) - batch_start_ns )))" echo "" if [[ ${#fail_list[@]} -gt 0 ]]; then echo " 失败用例:" for f in "${fail_list[@]}"; do echo " - $f" done echo "" fi if [[ $failed -gt 0 ]]; then echo "❌ 存在失败用例" exit 1 else echo "✅ 全部通过(跳过 $skipped 个)" exit 0 fi