| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- #!/usr/bin/env bash
- set -euo pipefail
- scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- cd "${scriptDir}"
- userMachineSet=0
- userCpuSet=0
- userTargetSet=0
- if [[ -n "${QEMU_MACHINE+x}" ]]; then userMachineSet=1; fi
- if [[ -n "${QEMU_CPU+x}" ]]; then userCpuSet=1; fi
- if [[ -n "${QEMU_TARGET+x}" ]]; then userTargetSet=1; fi
- qemuMode="${QEMU_MODE:-full}"
- qemuMachine="${QEMU_MACHINE:-mps2-an386}"
- qemuCpu="${QEMU_CPU:-cortex-m4}"
- qemuTimeoutSec="${QEMU_TIMEOUT_SEC:-120}"
- qemuTarget="${QEMU_TARGET:-RyanJsonQemu}"
- qemuForceClean="${QEMU_FORCE_CONFIG_CLEAN:-0}"
- qemuStopOnFail="${QEMU_STOP_ON_FAIL:-1}"
- qemuLogRoot="${QEMU_LOG_ROOT:-coverage/qemu}"
- qemuMemory="${QEMU_MEMORY:-64M}"
- qemuConsoleLog="${QEMU_CONSOLE_LOG:-1}"
- qemuSaveLog="${QEMU_SAVE_LOG:-0}"
- qemuMaxCases="${QEMU_MAX_CASES:-0}"
- require_cmd() {
- if ! command -v "$1" >/dev/null 2>&1; then
- echo "[错误] 缺少命令: $1"
- echo "[提示] 可执行: bash ./scripts/setup/install_qemu_deps.sh"
- exit 1
- fi
- }
- require_cmd xmake
- require_cmd arm-none-eabi-gcc
- require_cmd arm-none-eabi-objcopy
- require_cmd qemu-system-arm
- qemuSemihostingMode="legacy"
- if qemu-system-arm -help 2>&1 | grep -q -- "-semihosting-config"; then
- qemuSemihostingMode="config"
- fi
- machine_supported() {
- local machineName="$1"
- qemu-system-arm -machine help | awk '{print $1}' | grep -Fxq "${machineName}"
- }
- cleanQemuStream() {
- if command -v perl >/dev/null 2>&1; then
- # Normalize UART CRLF to LF so grep ^...$ marker checks are reliable.
- # Keep ANSI ESC (\x1B) for colored test output; strip other non-printable noise bytes.
- perl -ne 's/\r//g; s/[\x00-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F\x7F]//g; next if length($_) > 4096; print;'
- else
- tr -d '\000\r'
- fi
- }
- getQemuChildPid() {
- local parentPid="$1"
- pgrep -P "${parentPid}" qemu-system-arm | head -n 1 || true
- }
- stopQemuRun() {
- local parentPid="$1"
- local reason="$2"
- local childPid=""
- local waitDeadline=0
- childPid="$(getQemuChildPid "${parentPid}")"
- if [[ -n "${childPid}" ]]; then
- kill -TERM "${childPid}" >/dev/null 2>&1 || true
- fi
- waitDeadline=$((SECONDS + 3))
- while kill -0 "${parentPid}" >/dev/null 2>&1; do
- if ((SECONDS >= waitDeadline)); then
- break
- fi
- sleep 0.2
- done
- if kill -0 "${parentPid}" >/dev/null 2>&1; then
- kill -TERM "${parentPid}" >/dev/null 2>&1 || true
- sleep 1
- kill -KILL "${parentPid}" >/dev/null 2>&1 || true
- fi
- }
- # 优先 mps2-an386(CM4F),若当前 QEMU 不支持则自动回退到 mps2-an385(CM3)。
- if ! machine_supported "${qemuMachine}"; then
- if [[ "${qemuMachine}" == "mps2-an386" ]] && machine_supported "mps2-an385"; then
- echo "[信息] 当前 QEMU 不支持 mps2-an386,自动回退到 mps2-an385。"
- qemuMachine="mps2-an385"
- if [[ "${userCpuSet}" -eq 0 ]]; then
- qemuCpu="cortex-m3"
- fi
- if [[ "${userTargetSet}" -eq 0 ]]; then
- qemuTarget="RyanJsonQemuCm3"
- fi
- else
- echo "[错误] QEMU 不支持 machine=${qemuMachine}"
- echo "[提示] 可用 machine 列表:"
- qemu-system-arm -machine help | sed -n '1,120p'
- exit 1
- fi
- fi
- # 用户直接选择 an385 且未显式指定 target/cpu 时,自动切到 CM3 构建目标。
- if [[ "${qemuMachine}" == "mps2-an385" ]]; then
- if [[ "${userCpuSet}" -eq 0 ]]; then
- qemuCpu="cortex-m3"
- fi
- if [[ "${userTargetSet}" -eq 0 ]]; then
- qemuTarget="RyanJsonQemuCm3"
- fi
- fi
- caseList=()
- add_case() {
- caseList+=("$1 $2 $3")
- }
- case "${qemuMode}" in
- quick)
- add_case false true true
- add_case true false true
- ;;
- nightly)
- for strictKey in false true; do
- for addAtHead in false true; do
- add_case "${strictKey}" "${addAtHead}" true
- done
- done
- ;;
- full)
- for strictKey in false true; do
- for addAtHead in false true; do
- for scientific in false true; do
- add_case "${strictKey}" "${addAtHead}" "${scientific}"
- done
- done
- done
- ;;
- *)
- echo "[错误] QEMU_MODE 仅支持 quick/nightly/full,当前值:${qemuMode}"
- exit 1
- ;;
- esac
- if ! [[ "${qemuMaxCases}" =~ ^[0-9]+$ ]]; then
- echo "[错误] QEMU_MAX_CASES 仅支持非负整数,当前值:${qemuMaxCases}"
- exit 1
- fi
- if ! [[ "${qemuSaveLog}" =~ ^[01]$ ]]; then
- echo "[错误] QEMU_SAVE_LOG 仅支持 0/1,当前值:${qemuSaveLog}"
- exit 1
- fi
- if ! [[ "${qemuConsoleLog}" =~ ^[01]$ ]]; then
- echo "[错误] QEMU_CONSOLE_LOG 仅支持 0/1,当前值:${qemuConsoleLog}"
- exit 1
- fi
- if [[ "${qemuConsoleLog}" == "0" && "${qemuSaveLog}" == "0" ]]; then
- echo "[信息] QEMU_CONSOLE_LOG=0 且 QEMU_SAVE_LOG=0 无可见输出,自动切换 QEMU_CONSOLE_LOG=1。"
- qemuConsoleLog="1"
- fi
- if [[ "${qemuSaveLog}" == "1" ]]; then
- mkdir -p "${qemuLogRoot}"
- fi
- if (( qemuMaxCases > 0 )) && (( qemuMaxCases < ${#caseList[@]} )); then
- caseList=("${caseList[@]:0:qemuMaxCases}")
- fi
- echo "===================================================="
- echo "QEMU 本地链路启动(完整 localbase 单测 + 对齐语义)"
- echo " - MODE=${qemuMode}"
- echo " - TARGET=${qemuTarget}"
- echo " - MACHINE=${qemuMachine}"
- echo " - CPU=${qemuCpu}"
- echo " - TIMEOUT=${qemuTimeoutSec}s"
- echo " - LOG_ROOT=${qemuLogRoot}"
- echo " - MEMORY=${qemuMemory}"
- echo " - CONSOLE_LOG=${qemuConsoleLog}"
- echo " - SAVE_LOG=${qemuSaveLog}"
- echo " - SEMIHOSTING=${qemuSemihostingMode}"
- echo " - MAX_CASES=${qemuMaxCases}"
- echo "===================================================="
- totalCases="${#caseList[@]}"
- caseIndex=0
- failedCases=0
- logHasRequiredMarkers() {
- local logPath="$1"
- if ! grep -Eq "^\\[QEMU\\]\\[RESULT\\] UNIT_PASS code=0 tick=[0-9]+\r?$" "${logPath}"; then
- return 1
- fi
- if ! grep -Eq "^\\[QEMU\\]\\[ALIGN\\] aligned_access PASS read=0x[0-9A-Fa-f]+\r?$" "${logPath}"; then
- return 1
- fi
- if ! grep -Eq "^\\[QEMU\\]\\[ALIGN\\] unaligned_access TRIGGER addr=0x[0-9A-Fa-f]+\r?$" "${logPath}"; then
- return 1
- fi
- if ! grep -Eq "^\\[QEMU\\]\\[RESULT\\] EXPECTED_UNALIGNED_FAULT (cfsr|fallbackAddr)=0x[0-9A-Fa-f]+\r?$" "${logPath}"; then
- return 1
- fi
- if ! grep -Eq "^\\[QEMU\\]\\[HARDFAULT\\] CFSR=0x[0-9A-Fa-f]+ HFSR=0x[0-9A-Fa-f]+ BFAR=0x[0-9A-Fa-f]+ MMFAR=0x[0-9A-Fa-f]+\r?$" "${logPath}" \
- && ! grep -Eq "^\\[QEMU\\]\\[HARDFAULT\\] fallback_soft_trap_no_hw_fault addr=0x[0-9A-Fa-f]+\r?$" "${logPath}"; then
- return 1
- fi
- return 0
- }
- logHasFailureMarkers() {
- local logPath="$1"
- if grep -Eq "^\\[QEMU\\]\\[RESULT\\] UNIT_FAIL code=-?[0-9]+\r?$" "${logPath}"; then
- return 0
- fi
- return 1
- }
- cleanupCaseLogIfNeeded() {
- local keepCaseLog="$1"
- local caseLogPath="$2"
- if [[ "${keepCaseLog}" == "0" && -n "${caseLogPath}" ]]; then
- rm -f "${caseLogPath}" >/dev/null 2>&1 || true
- fi
- }
- run_case() {
- local strictKey="$1"
- local addAtHead="$2"
- local scientific="$3"
- local caseName="strict_${strictKey}__head_${addAtHead}__sci_${scientific}"
- local caseLogPath=""
- local buildLogPath=""
- local keepCaseLog="0"
- local deadlineSec=0
- local qemuPid=0
- local qemuRc=0
- echo "----------------------------------------------------"
- echo "[用例] ${caseName}"
- echo " - RyanJsonStrictObjectKeyCheck=${strictKey}"
- echo " - RyanJsonDefaultAddAtHead=${addAtHead}"
- echo " - RyanJsonSnprintfSupportScientific=${scientific}"
- echo "----------------------------------------------------"
- export RYANJSON_STRICT_OBJECT_KEY_CHECK="${strictKey}"
- export RYANJSON_DEFAULT_ADD_AT_HEAD="${addAtHead}"
- export RYANJSON_SNPRINTF_SUPPORT_SCIENTIFIC="${scientific}"
- if [[ "${qemuForceClean}" == "1" ]]; then
- xmake f -c -p cross -a arm
- else
- xmake f -p cross -a arm
- fi
- # 清理旧产物,避免构建失败时误用历史 ELF。
- find ./build -type f -name "${qemuTarget}.elf" -delete >/dev/null 2>&1 || true
- buildLogPath="$(mktemp "/tmp/${caseName}.build.XXXX.log")"
- if ! xmake -b "${qemuTarget}" 2>&1 | tee "${buildLogPath}"; then
- echo "[错误] xmake 构建失败,已终止本用例并跳过 QEMU 运行。"
- tail -n 120 "${buildLogPath}" || true
- rm -f "${buildLogPath}" >/dev/null 2>&1 || true
- return 1
- fi
- if grep -Eq '(^|[^[:alpha:]])error:' "${buildLogPath}"; then
- echo "[错误] 构建日志检测到编译错误,已终止本用例并跳过 QEMU 运行。"
- tail -n 120 "${buildLogPath}" || true
- rm -f "${buildLogPath}" >/dev/null 2>&1 || true
- return 1
- fi
- rm -f "${buildLogPath}" >/dev/null 2>&1 || true
- local elfPath
- elfPath="$(find ./build -type f -name "${qemuTarget}.elf" | head -n 1 || true)"
- if [[ -z "${elfPath}" ]]; then
- echo "[错误] 未找到 ELF 输出(${qemuTarget}.elf)"
- cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
- return 1
- fi
- if [[ "${qemuSaveLog}" == "1" ]]; then
- caseLogPath="${qemuLogRoot}/${caseName}.log"
- keepCaseLog="1"
- else
- caseLogPath="$(mktemp "/tmp/${caseName}.XXXX.log")"
- keepCaseLog="0"
- fi
- echo "[信息] ELF: ${elfPath}"
- if [[ "${qemuSaveLog}" == "1" ]]; then
- echo "[阶段] 启动 QEMU 并抓取日志 -> ${caseLogPath}"
- else
- echo "[阶段] 启动 QEMU(终端实时输出,日志不落盘)"
- fi
- local -a qemuArgs=(
- -M "${qemuMachine}"
- -cpu "${qemuCpu}"
- -nographic
- -kernel "${elfPath}"
- )
- if [[ "${qemuSemihostingMode}" == "config" ]]; then
- qemuArgs+=(-semihosting-config enable=on,target=native)
- else
- qemuArgs+=(-semihosting)
- fi
- if [[ -n "${qemuMemory}" ]]; then
- qemuArgs+=(-m "${qemuMemory}")
- fi
- : > "${caseLogPath}"
- deadlineSec=$((SECONDS + qemuTimeoutSec))
- set +e
- if [[ "${qemuConsoleLog}" == "1" ]]; then
- (
- qemu-system-arm "${qemuArgs[@]}" 2>&1 | cleanQemuStream | tee -a "${caseLogPath}"
- ) &
- else
- (
- qemu-system-arm "${qemuArgs[@]}" 2>&1 | cleanQemuStream >> "${caseLogPath}"
- ) &
- fi
- qemuPid=$!
- while true; do
- if ! kill -0 "${qemuPid}" >/dev/null 2>&1; then
- wait "${qemuPid}"
- qemuRc=$?
- break
- fi
- if ((SECONDS >= deadlineSec)); then
- stopQemuRun "${qemuPid}" "timeout"
- wait "${qemuPid}"
- qemuRc=124
- break
- fi
- sleep 1
- done
- set -e
- # Give the log pipeline a tiny grace window to flush trailing bytes.
- sleep 0.1
- if [[ "${qemuRc}" -eq 124 ]]; then
- echo "[错误] QEMU 超时(${qemuTimeoutSec}s)"
- tail -n 120 "${caseLogPath}"
- cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
- return 1
- fi
- if logHasFailureMarkers "${caseLogPath}"; then
- echo "[错误] 用例失败(检测到 [QEMU][RESULT] UNIT_FAIL)"
- tail -n 120 "${caseLogPath}"
- cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
- return 1
- fi
- local missing=0
- if ! logHasRequiredMarkers "${caseLogPath}"; then
- if ! grep -Eq "^\\[QEMU\\]\\[RESULT\\] UNIT_PASS code=0 tick=[0-9]+\r?$" "${caseLogPath}"; then
- echo "[错误] 日志缺少关键标记: [QEMU][RESULT] UNIT_PASS code=0 tick=<num>"
- fi
- if ! grep -Eq "^\\[QEMU\\]\\[ALIGN\\] aligned_access PASS read=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}"; then
- echo "[错误] 日志缺少关键标记: [QEMU][ALIGN] aligned_access PASS read=0x<hex>"
- fi
- if ! grep -Eq "^\\[QEMU\\]\\[ALIGN\\] unaligned_access TRIGGER addr=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}"; then
- echo "[错误] 日志缺少关键标记: [QEMU][ALIGN] unaligned_access TRIGGER addr=0x<hex>"
- fi
- if ! grep -Eq "^\\[QEMU\\]\\[RESULT\\] EXPECTED_UNALIGNED_FAULT (cfsr|fallbackAddr)=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}"; then
- echo "[错误] 日志缺少关键标记: [QEMU][RESULT] EXPECTED_UNALIGNED_FAULT <dynamic>"
- fi
- if ! grep -Eq "^\\[QEMU\\]\\[HARDFAULT\\] CFSR=0x[0-9A-Fa-f]+ HFSR=0x[0-9A-Fa-f]+ BFAR=0x[0-9A-Fa-f]+ MMFAR=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}" \
- && ! grep -Eq "^\\[QEMU\\]\\[HARDFAULT\\] fallback_soft_trap_no_hw_fault addr=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}"; then
- echo "[错误] 日志缺少 fault 现场标记(HARDFAULT cfsr/hfsr 或 fallback addr)"
- fi
- missing=1
- fi
- if [[ "${missing}" -ne 0 ]]; then
- echo "[错误] 用例失败,日志尾部:"
- tail -n 120 "${caseLogPath}"
- cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
- return 1
- fi
- cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
- echo "[通过] ${caseName}"
- return 0
- }
- for entry in "${caseList[@]}"; do
- caseIndex=$((caseIndex + 1))
- read -r strictKey addAtHead scientific <<< "${entry}"
- echo
- echo "===================================================="
- echo "【QEMU 用例 ${caseIndex}/${totalCases}】"
- echo "===================================================="
- if run_case "${strictKey}" "${addAtHead}" "${scientific}"; then
- :
- else
- failedCases=$((failedCases + 1))
- if [[ "${qemuStopOnFail}" == "1" ]]; then
- echo "[错误] 按 QEMU_STOP_ON_FAIL=1 提前终止。"
- exit 1
- fi
- fi
- done
- echo
- echo "QEMU 单测矩阵执行完成。"
- echo " - 模式: ${qemuMode}"
- echo " - 总用例: ${totalCases}"
- echo " - 失败用例: ${failedCases}"
- if [[ "${qemuSaveLog}" == "1" ]]; then
- echo " - 日志目录: ${qemuLogRoot}"
- else
- echo " - 日志输出: 终端实时输出(不落盘)"
- fi
- if [[ "${failedCases}" -gt 0 ]]; then
- exit 1
- fi
|