run_local_qemu.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. cd "${scriptDir}"
  5. userMachineSet=0
  6. userCpuSet=0
  7. userTargetSet=0
  8. if [[ -n "${QEMU_MACHINE+x}" ]]; then userMachineSet=1; fi
  9. if [[ -n "${QEMU_CPU+x}" ]]; then userCpuSet=1; fi
  10. if [[ -n "${QEMU_TARGET+x}" ]]; then userTargetSet=1; fi
  11. qemuMode="${QEMU_MODE:-full}"
  12. qemuMachine="${QEMU_MACHINE:-mps2-an386}"
  13. qemuCpu="${QEMU_CPU:-cortex-m4}"
  14. qemuTimeoutSec="${QEMU_TIMEOUT_SEC:-120}"
  15. qemuTarget="${QEMU_TARGET:-RyanJsonQemu}"
  16. qemuForceClean="${QEMU_FORCE_CONFIG_CLEAN:-0}"
  17. qemuStopOnFail="${QEMU_STOP_ON_FAIL:-1}"
  18. qemuLogRoot="${QEMU_LOG_ROOT:-coverage/qemu}"
  19. qemuMemory="${QEMU_MEMORY:-64M}"
  20. qemuConsoleLog="${QEMU_CONSOLE_LOG:-1}"
  21. qemuSaveLog="${QEMU_SAVE_LOG:-0}"
  22. qemuMaxCases="${QEMU_MAX_CASES:-0}"
  23. require_cmd() {
  24. if ! command -v "$1" >/dev/null 2>&1; then
  25. echo "[错误] 缺少命令: $1"
  26. echo "[提示] 可执行: bash ./scripts/setup/install_qemu_deps.sh"
  27. exit 1
  28. fi
  29. }
  30. require_cmd xmake
  31. require_cmd arm-none-eabi-gcc
  32. require_cmd arm-none-eabi-objcopy
  33. require_cmd qemu-system-arm
  34. qemuSemihostingMode="legacy"
  35. if qemu-system-arm -help 2>&1 | grep -q -- "-semihosting-config"; then
  36. qemuSemihostingMode="config"
  37. fi
  38. machine_supported() {
  39. local machineName="$1"
  40. qemu-system-arm -machine help | awk '{print $1}' | grep -Fxq "${machineName}"
  41. }
  42. cleanQemuStream() {
  43. if command -v perl >/dev/null 2>&1; then
  44. # Normalize UART CRLF to LF so grep ^...$ marker checks are reliable.
  45. # Keep ANSI ESC (\x1B) for colored test output; strip other non-printable noise bytes.
  46. perl -ne 's/\r//g; s/[\x00-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F\x7F]//g; next if length($_) > 4096; print;'
  47. else
  48. tr -d '\000\r'
  49. fi
  50. }
  51. getQemuChildPid() {
  52. local parentPid="$1"
  53. pgrep -P "${parentPid}" qemu-system-arm | head -n 1 || true
  54. }
  55. stopQemuRun() {
  56. local parentPid="$1"
  57. local reason="$2"
  58. local childPid=""
  59. local waitDeadline=0
  60. childPid="$(getQemuChildPid "${parentPid}")"
  61. if [[ -n "${childPid}" ]]; then
  62. kill -TERM "${childPid}" >/dev/null 2>&1 || true
  63. fi
  64. waitDeadline=$((SECONDS + 3))
  65. while kill -0 "${parentPid}" >/dev/null 2>&1; do
  66. if ((SECONDS >= waitDeadline)); then
  67. break
  68. fi
  69. sleep 0.2
  70. done
  71. if kill -0 "${parentPid}" >/dev/null 2>&1; then
  72. kill -TERM "${parentPid}" >/dev/null 2>&1 || true
  73. sleep 1
  74. kill -KILL "${parentPid}" >/dev/null 2>&1 || true
  75. fi
  76. }
  77. # 优先 mps2-an386(CM4F),若当前 QEMU 不支持则自动回退到 mps2-an385(CM3)。
  78. if ! machine_supported "${qemuMachine}"; then
  79. if [[ "${qemuMachine}" == "mps2-an386" ]] && machine_supported "mps2-an385"; then
  80. echo "[信息] 当前 QEMU 不支持 mps2-an386,自动回退到 mps2-an385。"
  81. qemuMachine="mps2-an385"
  82. if [[ "${userCpuSet}" -eq 0 ]]; then
  83. qemuCpu="cortex-m3"
  84. fi
  85. if [[ "${userTargetSet}" -eq 0 ]]; then
  86. qemuTarget="RyanJsonQemuCm3"
  87. fi
  88. else
  89. echo "[错误] QEMU 不支持 machine=${qemuMachine}"
  90. echo "[提示] 可用 machine 列表:"
  91. qemu-system-arm -machine help | sed -n '1,120p'
  92. exit 1
  93. fi
  94. fi
  95. # 用户直接选择 an385 且未显式指定 target/cpu 时,自动切到 CM3 构建目标。
  96. if [[ "${qemuMachine}" == "mps2-an385" ]]; then
  97. if [[ "${userCpuSet}" -eq 0 ]]; then
  98. qemuCpu="cortex-m3"
  99. fi
  100. if [[ "${userTargetSet}" -eq 0 ]]; then
  101. qemuTarget="RyanJsonQemuCm3"
  102. fi
  103. fi
  104. caseList=()
  105. add_case() {
  106. caseList+=("$1 $2 $3")
  107. }
  108. case "${qemuMode}" in
  109. quick)
  110. add_case false true true
  111. add_case true false true
  112. ;;
  113. nightly)
  114. for strictKey in false true; do
  115. for addAtHead in false true; do
  116. add_case "${strictKey}" "${addAtHead}" true
  117. done
  118. done
  119. ;;
  120. full)
  121. for strictKey in false true; do
  122. for addAtHead in false true; do
  123. for scientific in false true; do
  124. add_case "${strictKey}" "${addAtHead}" "${scientific}"
  125. done
  126. done
  127. done
  128. ;;
  129. *)
  130. echo "[错误] QEMU_MODE 仅支持 quick/nightly/full,当前值:${qemuMode}"
  131. exit 1
  132. ;;
  133. esac
  134. if ! [[ "${qemuMaxCases}" =~ ^[0-9]+$ ]]; then
  135. echo "[错误] QEMU_MAX_CASES 仅支持非负整数,当前值:${qemuMaxCases}"
  136. exit 1
  137. fi
  138. if ! [[ "${qemuSaveLog}" =~ ^[01]$ ]]; then
  139. echo "[错误] QEMU_SAVE_LOG 仅支持 0/1,当前值:${qemuSaveLog}"
  140. exit 1
  141. fi
  142. if ! [[ "${qemuConsoleLog}" =~ ^[01]$ ]]; then
  143. echo "[错误] QEMU_CONSOLE_LOG 仅支持 0/1,当前值:${qemuConsoleLog}"
  144. exit 1
  145. fi
  146. if [[ "${qemuConsoleLog}" == "0" && "${qemuSaveLog}" == "0" ]]; then
  147. echo "[信息] QEMU_CONSOLE_LOG=0 且 QEMU_SAVE_LOG=0 无可见输出,自动切换 QEMU_CONSOLE_LOG=1。"
  148. qemuConsoleLog="1"
  149. fi
  150. if [[ "${qemuSaveLog}" == "1" ]]; then
  151. mkdir -p "${qemuLogRoot}"
  152. fi
  153. if (( qemuMaxCases > 0 )) && (( qemuMaxCases < ${#caseList[@]} )); then
  154. caseList=("${caseList[@]:0:qemuMaxCases}")
  155. fi
  156. echo "===================================================="
  157. echo "QEMU 本地链路启动(完整 localbase 单测 + 对齐语义)"
  158. echo " - MODE=${qemuMode}"
  159. echo " - TARGET=${qemuTarget}"
  160. echo " - MACHINE=${qemuMachine}"
  161. echo " - CPU=${qemuCpu}"
  162. echo " - TIMEOUT=${qemuTimeoutSec}s"
  163. echo " - LOG_ROOT=${qemuLogRoot}"
  164. echo " - MEMORY=${qemuMemory}"
  165. echo " - CONSOLE_LOG=${qemuConsoleLog}"
  166. echo " - SAVE_LOG=${qemuSaveLog}"
  167. echo " - SEMIHOSTING=${qemuSemihostingMode}"
  168. echo " - MAX_CASES=${qemuMaxCases}"
  169. echo "===================================================="
  170. totalCases="${#caseList[@]}"
  171. caseIndex=0
  172. failedCases=0
  173. logHasRequiredMarkers() {
  174. local logPath="$1"
  175. if ! grep -Eq "^\\[QEMU\\]\\[RESULT\\] UNIT_PASS code=0 tick=[0-9]+\r?$" "${logPath}"; then
  176. return 1
  177. fi
  178. if ! grep -Eq "^\\[QEMU\\]\\[ALIGN\\] aligned_access PASS read=0x[0-9A-Fa-f]+\r?$" "${logPath}"; then
  179. return 1
  180. fi
  181. if ! grep -Eq "^\\[QEMU\\]\\[ALIGN\\] unaligned_access TRIGGER addr=0x[0-9A-Fa-f]+\r?$" "${logPath}"; then
  182. return 1
  183. fi
  184. if ! grep -Eq "^\\[QEMU\\]\\[RESULT\\] EXPECTED_UNALIGNED_FAULT (cfsr|fallbackAddr)=0x[0-9A-Fa-f]+\r?$" "${logPath}"; then
  185. return 1
  186. fi
  187. 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}" \
  188. && ! grep -Eq "^\\[QEMU\\]\\[HARDFAULT\\] fallback_soft_trap_no_hw_fault addr=0x[0-9A-Fa-f]+\r?$" "${logPath}"; then
  189. return 1
  190. fi
  191. return 0
  192. }
  193. logHasFailureMarkers() {
  194. local logPath="$1"
  195. if grep -Eq "^\\[QEMU\\]\\[RESULT\\] UNIT_FAIL code=-?[0-9]+\r?$" "${logPath}"; then
  196. return 0
  197. fi
  198. return 1
  199. }
  200. cleanupCaseLogIfNeeded() {
  201. local keepCaseLog="$1"
  202. local caseLogPath="$2"
  203. if [[ "${keepCaseLog}" == "0" && -n "${caseLogPath}" ]]; then
  204. rm -f "${caseLogPath}" >/dev/null 2>&1 || true
  205. fi
  206. }
  207. run_case() {
  208. local strictKey="$1"
  209. local addAtHead="$2"
  210. local scientific="$3"
  211. local caseName="strict_${strictKey}__head_${addAtHead}__sci_${scientific}"
  212. local caseLogPath=""
  213. local buildLogPath=""
  214. local keepCaseLog="0"
  215. local deadlineSec=0
  216. local qemuPid=0
  217. local qemuRc=0
  218. echo "----------------------------------------------------"
  219. echo "[用例] ${caseName}"
  220. echo " - RyanJsonStrictObjectKeyCheck=${strictKey}"
  221. echo " - RyanJsonDefaultAddAtHead=${addAtHead}"
  222. echo " - RyanJsonSnprintfSupportScientific=${scientific}"
  223. echo "----------------------------------------------------"
  224. export RYANJSON_STRICT_OBJECT_KEY_CHECK="${strictKey}"
  225. export RYANJSON_DEFAULT_ADD_AT_HEAD="${addAtHead}"
  226. export RYANJSON_SNPRINTF_SUPPORT_SCIENTIFIC="${scientific}"
  227. if [[ "${qemuForceClean}" == "1" ]]; then
  228. xmake f -c -p cross -a arm
  229. else
  230. xmake f -p cross -a arm
  231. fi
  232. # 清理旧产物,避免构建失败时误用历史 ELF。
  233. find ./build -type f -name "${qemuTarget}.elf" -delete >/dev/null 2>&1 || true
  234. buildLogPath="$(mktemp "/tmp/${caseName}.build.XXXX.log")"
  235. if ! xmake -b "${qemuTarget}" 2>&1 | tee "${buildLogPath}"; then
  236. echo "[错误] xmake 构建失败,已终止本用例并跳过 QEMU 运行。"
  237. tail -n 120 "${buildLogPath}" || true
  238. rm -f "${buildLogPath}" >/dev/null 2>&1 || true
  239. return 1
  240. fi
  241. if grep -Eq '(^|[^[:alpha:]])error:' "${buildLogPath}"; then
  242. echo "[错误] 构建日志检测到编译错误,已终止本用例并跳过 QEMU 运行。"
  243. tail -n 120 "${buildLogPath}" || true
  244. rm -f "${buildLogPath}" >/dev/null 2>&1 || true
  245. return 1
  246. fi
  247. rm -f "${buildLogPath}" >/dev/null 2>&1 || true
  248. local elfPath
  249. elfPath="$(find ./build -type f -name "${qemuTarget}.elf" | head -n 1 || true)"
  250. if [[ -z "${elfPath}" ]]; then
  251. echo "[错误] 未找到 ELF 输出(${qemuTarget}.elf)"
  252. cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
  253. return 1
  254. fi
  255. if [[ "${qemuSaveLog}" == "1" ]]; then
  256. caseLogPath="${qemuLogRoot}/${caseName}.log"
  257. keepCaseLog="1"
  258. else
  259. caseLogPath="$(mktemp "/tmp/${caseName}.XXXX.log")"
  260. keepCaseLog="0"
  261. fi
  262. echo "[信息] ELF: ${elfPath}"
  263. if [[ "${qemuSaveLog}" == "1" ]]; then
  264. echo "[阶段] 启动 QEMU 并抓取日志 -> ${caseLogPath}"
  265. else
  266. echo "[阶段] 启动 QEMU(终端实时输出,日志不落盘)"
  267. fi
  268. local -a qemuArgs=(
  269. -M "${qemuMachine}"
  270. -cpu "${qemuCpu}"
  271. -nographic
  272. -kernel "${elfPath}"
  273. )
  274. if [[ "${qemuSemihostingMode}" == "config" ]]; then
  275. qemuArgs+=(-semihosting-config enable=on,target=native)
  276. else
  277. qemuArgs+=(-semihosting)
  278. fi
  279. if [[ -n "${qemuMemory}" ]]; then
  280. qemuArgs+=(-m "${qemuMemory}")
  281. fi
  282. : > "${caseLogPath}"
  283. deadlineSec=$((SECONDS + qemuTimeoutSec))
  284. set +e
  285. if [[ "${qemuConsoleLog}" == "1" ]]; then
  286. (
  287. qemu-system-arm "${qemuArgs[@]}" 2>&1 | cleanQemuStream | tee -a "${caseLogPath}"
  288. ) &
  289. else
  290. (
  291. qemu-system-arm "${qemuArgs[@]}" 2>&1 | cleanQemuStream >> "${caseLogPath}"
  292. ) &
  293. fi
  294. qemuPid=$!
  295. while true; do
  296. if ! kill -0 "${qemuPid}" >/dev/null 2>&1; then
  297. wait "${qemuPid}"
  298. qemuRc=$?
  299. break
  300. fi
  301. if ((SECONDS >= deadlineSec)); then
  302. stopQemuRun "${qemuPid}" "timeout"
  303. wait "${qemuPid}"
  304. qemuRc=124
  305. break
  306. fi
  307. sleep 1
  308. done
  309. set -e
  310. # Give the log pipeline a tiny grace window to flush trailing bytes.
  311. sleep 0.1
  312. if [[ "${qemuRc}" -eq 124 ]]; then
  313. echo "[错误] QEMU 超时(${qemuTimeoutSec}s)"
  314. tail -n 120 "${caseLogPath}"
  315. cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
  316. return 1
  317. fi
  318. if logHasFailureMarkers "${caseLogPath}"; then
  319. echo "[错误] 用例失败(检测到 [QEMU][RESULT] UNIT_FAIL)"
  320. tail -n 120 "${caseLogPath}"
  321. cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
  322. return 1
  323. fi
  324. local missing=0
  325. if ! logHasRequiredMarkers "${caseLogPath}"; then
  326. if ! grep -Eq "^\\[QEMU\\]\\[RESULT\\] UNIT_PASS code=0 tick=[0-9]+\r?$" "${caseLogPath}"; then
  327. echo "[错误] 日志缺少关键标记: [QEMU][RESULT] UNIT_PASS code=0 tick=<num>"
  328. fi
  329. if ! grep -Eq "^\\[QEMU\\]\\[ALIGN\\] aligned_access PASS read=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}"; then
  330. echo "[错误] 日志缺少关键标记: [QEMU][ALIGN] aligned_access PASS read=0x<hex>"
  331. fi
  332. if ! grep -Eq "^\\[QEMU\\]\\[ALIGN\\] unaligned_access TRIGGER addr=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}"; then
  333. echo "[错误] 日志缺少关键标记: [QEMU][ALIGN] unaligned_access TRIGGER addr=0x<hex>"
  334. fi
  335. if ! grep -Eq "^\\[QEMU\\]\\[RESULT\\] EXPECTED_UNALIGNED_FAULT (cfsr|fallbackAddr)=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}"; then
  336. echo "[错误] 日志缺少关键标记: [QEMU][RESULT] EXPECTED_UNALIGNED_FAULT <dynamic>"
  337. fi
  338. 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}" \
  339. && ! grep -Eq "^\\[QEMU\\]\\[HARDFAULT\\] fallback_soft_trap_no_hw_fault addr=0x[0-9A-Fa-f]+\r?$" "${caseLogPath}"; then
  340. echo "[错误] 日志缺少 fault 现场标记(HARDFAULT cfsr/hfsr 或 fallback addr)"
  341. fi
  342. missing=1
  343. fi
  344. if [[ "${missing}" -ne 0 ]]; then
  345. echo "[错误] 用例失败,日志尾部:"
  346. tail -n 120 "${caseLogPath}"
  347. cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
  348. return 1
  349. fi
  350. cleanupCaseLogIfNeeded "${keepCaseLog}" "${caseLogPath}"
  351. echo "[通过] ${caseName}"
  352. return 0
  353. }
  354. for entry in "${caseList[@]}"; do
  355. caseIndex=$((caseIndex + 1))
  356. read -r strictKey addAtHead scientific <<< "${entry}"
  357. echo
  358. echo "===================================================="
  359. echo "【QEMU 用例 ${caseIndex}/${totalCases}】"
  360. echo "===================================================="
  361. if run_case "${strictKey}" "${addAtHead}" "${scientific}"; then
  362. :
  363. else
  364. failedCases=$((failedCases + 1))
  365. if [[ "${qemuStopOnFail}" == "1" ]]; then
  366. echo "[错误] 按 QEMU_STOP_ON_FAIL=1 提前终止。"
  367. exit 1
  368. fi
  369. fi
  370. done
  371. echo
  372. echo "QEMU 单测矩阵执行完成。"
  373. echo " - 模式: ${qemuMode}"
  374. echo " - 总用例: ${totalCases}"
  375. echo " - 失败用例: ${failedCases}"
  376. if [[ "${qemuSaveLog}" == "1" ]]; then
  377. echo " - 日志目录: ${qemuLogRoot}"
  378. else
  379. echo " - 日志输出: 终端实时输出(不落盘)"
  380. fi
  381. if [[ "${failedCases}" -gt 0 ]]; then
  382. exit 1
  383. fi