run_local_base.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. #!/bin/bash
  2. set -euo pipefail
  3. # 本地一键 Base(单元测试矩阵)。
  4. # 默认值:
  5. # UNIT_MODE=full(8 组配置全覆盖)
  6. # UNIT_SKIP_COV=1(跳过覆盖率,提速)
  7. # UNIT_STOP_ON_FAIL=1(首个失败立即退出)
  8. # XMAKE_FORCE_CLEAN=0(增量配置,减少重编译)
  9. # UNIT_SYNC_ONLY=0(仅同步 runner 列表,不跑测试)
  10. # UNIT_SINGLE_CASE=""(可选,格式: "false false true" 覆盖语义矩阵)
  11. # 以上参数都可用同名环境变量临时覆盖。
  12. scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  13. # shellcheck source=scripts/lib/common.sh
  14. source "${scriptDir}/scripts/lib/common.sh"
  15. repoRoot="$(ryanjson_repo_root_from_source "${BASH_SOURCE[0]}" 0)"
  16. cd "${repoRoot}"
  17. ryanjson_normalize_bool() {
  18. # 统一布尔值归一化(true/false)
  19. local name="$1"
  20. local value="$2"
  21. local lower=""
  22. lower="$(printf '%s' "${value}" | tr '[:upper:]' '[:lower:]')"
  23. case "${lower}" in
  24. true|1|on|yes)
  25. printf 'true\n'
  26. return 0
  27. ;;
  28. false|0|off|no)
  29. printf 'false\n'
  30. return 0
  31. ;;
  32. *)
  33. ryanjson_log_error "${name} 仅支持 true/false/1/0/on/off/yes/no,当前值:${value}"
  34. return 1
  35. ;;
  36. esac
  37. }
  38. ryanjson_parse_single_case() {
  39. local raw="$1"
  40. local strictKey=""
  41. local addAtHead=""
  42. local scientific=""
  43. local extra=""
  44. read -r strictKey addAtHead scientific extra <<< "${raw}"
  45. if [[ -z "${strictKey}" || -z "${addAtHead}" || -z "${scientific}" || -n "${extra}" ]]; then
  46. ryanjson_log_error "UNIT_SINGLE_CASE 格式错误,应为:\"<strict> <head> <sci>\""
  47. return 1
  48. fi
  49. strictKey="$(ryanjson_normalize_bool "UNIT_SINGLE_CASE.strict" "${strictKey}")" || return 1
  50. addAtHead="$(ryanjson_normalize_bool "UNIT_SINGLE_CASE.head" "${addAtHead}")" || return 1
  51. scientific="$(ryanjson_normalize_bool "UNIT_SINGLE_CASE.sci" "${scientific}")" || return 1
  52. printf '%s %s %s\n' "${strictKey}" "${addAtHead}" "${scientific}"
  53. }
  54. ryanjson_sync_unity_runner_list() {
  55. # 同步 Unity runner 列表(确保 test_list.inc 与用例实现一致)
  56. local listFile="${repoRoot}/test/unityTest/runner/test_list.inc"
  57. local caseRoot="${repoRoot}/test/unityTest/cases"
  58. if [[ ! -d "${caseRoot}" ]]; then
  59. ryanjson_log_error "未找到 unity cases 目录: ${caseRoot}"
  60. return 1
  61. fi
  62. # 收集所有 Runner 函数实现
  63. local runnerPattern='void[[:space:]]+test[A-Za-z0-9_]+Runner[[:space:]]*\('
  64. local -a implLines=()
  65. mapfile -t implLines < <(grep -R -n -E "${runnerPattern}" "${caseRoot}" || true)
  66. if [[ ${#implLines[@]} -eq 0 ]]; then
  67. ryanjson_log_error "未找到任何 runner 实现: ${caseRoot}"
  68. return 1
  69. fi
  70. # 统一排序并去重(按文件、行号顺序)
  71. declare -A seen=()
  72. declare -a implemented=()
  73. local -a implSorted=()
  74. mapfile -t implSorted < <(
  75. printf '%s\n' "${implLines[@]}" | awk -F: '
  76. {
  77. file = $1;
  78. line = $2;
  79. if (match($0, /void[[:space:]]+(test[A-Za-z0-9_]+Runner)[[:space:]]*\(/, m)) {
  80. print file "\t" line "\t" m[1];
  81. }
  82. }
  83. ' | sort -t $'\t' -k1,1 -k2,2n
  84. )
  85. for line in "${implSorted[@]}"; do
  86. local name=""
  87. name="$(echo "${line}" | cut -f3)"
  88. if [[ -n "${name}" && -z "${seen[${name}]+x}" ]]; then
  89. implemented+=("${name}")
  90. seen["${name}"]=1
  91. fi
  92. done
  93. # 生成新的 list 文件
  94. local tmpFile=""
  95. local tmpRoot="${repoRoot}/localLogs/_tmp"
  96. mkdir -p "${tmpRoot}"
  97. tmpFile="$(mktemp "${tmpRoot}/unity_runner.XXXXXX")"
  98. {
  99. echo "// Auto-generated by run_local_base.sh. Do not edit by hand."
  100. for name in "${implemented[@]}"; do
  101. if [[ "${name}" == "testRfc8259Runner" ]]; then
  102. echo "#if !defined(RyanJsonTestPlatformQemu)"
  103. echo "UNITY_TEST_LIST_ENTRY(${name})"
  104. echo "#endif"
  105. else
  106. echo "UNITY_TEST_LIST_ENTRY(${name})"
  107. fi
  108. done
  109. } > "${tmpFile}"
  110. # 对比并更新
  111. if [[ ! -f "${listFile}" ]] || ! cmp -s "${tmpFile}" "${listFile}"; then
  112. mv "${tmpFile}" "${listFile}"
  113. ryanjson_log_info "Unity runner 列表已更新: ${listFile}"
  114. else
  115. rm -f "${tmpFile}"
  116. ryanjson_log_done "Unity runner 列表已是最新"
  117. fi
  118. }
  119. ryanjson_unit_run_case() {
  120. # 单用例执行:构建 -> 运行 ->(可选)覆盖率
  121. local index="$1"
  122. local total="$2"
  123. local strictKey="$3"
  124. local addAtHead="$4"
  125. local scientific="$5"
  126. local caseName=""
  127. local profraw=""
  128. caseName="$(ryanjson_semantic_case_name "${strictKey}" "${addAtHead}" "${scientific}")"
  129. profraw="${profileRoot}/${caseName}.profraw"
  130. ryanjson_print_banner_begin "【用例 ${index}/${total}】${caseName}"
  131. ryanjson_print_semantic_kv "${strictKey}" "${addAtHead}" "${scientific}"
  132. ryanjson_print_banner_end
  133. # 语义宏注入(影响编译与运行路径)
  134. ryanjson_export_semantic_macros "${strictKey}" "${addAtHead}" "${scientific}"
  135. # 配置与构建
  136. if ! ryanjson_run_xmake_config "${xmakeForceClean}" "${caseName}"; then
  137. return 1
  138. fi
  139. if ! ryanjson_run_xmake_build "RyanJson" "${caseName}"; then
  140. return 1
  141. fi
  142. # 运行测试二进制
  143. ryanjson_log_phase "正在运行单元测试二进制..."
  144. if ! LLVM_PROFILE_FILE="${profraw}" ./build/linux/x86/release/RyanJson; then
  145. ryanjson_log_error "单元测试执行失败:${caseName}"
  146. return 1
  147. fi
  148. # 覆盖率由外层统一处理
  149. if [[ "${unitSkipCov}" == "1" ]]; then
  150. ryanjson_log_info "UNIT_SKIP_COV=1,已跳过覆盖率生成。"
  151. return 0
  152. fi
  153. }
  154. ryanjson_unit_generate_coverage() {
  155. # 汇总覆盖率报告
  156. local mergedProfdata=""
  157. local reportTxt=""
  158. local reportHtml=""
  159. local -a profrawFiles=()
  160. if ! command -v llvm-profdata >/dev/null 2>&1; then
  161. ryanjson_log_error "未找到 llvm-profdata,无法生成覆盖率。"
  162. return 1
  163. fi
  164. if ! command -v llvm-cov >/dev/null 2>&1; then
  165. ryanjson_log_error "未找到 llvm-cov,无法生成覆盖率。"
  166. return 1
  167. fi
  168. shopt -s nullglob
  169. profrawFiles=("${profileRoot}"/*.profraw)
  170. shopt -u nullglob
  171. if [[ "${#profrawFiles[@]}" -eq 0 ]]; then
  172. ryanjson_log_error "未找到可合并的 profraw 文件。"
  173. return 1
  174. fi
  175. mergedProfdata="${coverageRoot}/coverage.profdata"
  176. reportTxt="${coverageRoot}/report.txt"
  177. reportHtml="${coverageRoot}/html"
  178. if ! llvm-profdata merge -sparse "${profrawFiles[@]}" -o "${mergedProfdata}"; then
  179. ryanjson_log_error "profraw 合并失败。"
  180. return 1
  181. fi
  182. if ! llvm-cov report ./build/linux/x86/release/RyanJson \
  183. -instr-profile="${mergedProfdata}" \
  184. -show-mcdc-summary \
  185. -sources ./RyanJson > "${reportTxt}"; then
  186. ryanjson_log_error "文本覆盖率生成失败。"
  187. return 1
  188. fi
  189. if ! llvm-cov show ./build/linux/x86/release/RyanJson \
  190. -instr-profile="${mergedProfdata}" \
  191. -format=html \
  192. -output-dir="${reportHtml}" \
  193. -show-mcdc-summary \
  194. -show-branches=count \
  195. -show-expansions \
  196. -show-regions \
  197. -show-line-counts-or-regions \
  198. -sources ./RyanJson; then
  199. ryanjson_log_error "HTML 覆盖率生成失败。"
  200. return 1
  201. fi
  202. }
  203. ryanjson_unit_main() {
  204. # 读取参数与默认值(保持与旧脚本一致)
  205. unitMode="${UNIT_MODE:-full}"
  206. unitSkipCov="${UNIT_SKIP_COV:-1}"
  207. unitStopOnFail="${UNIT_STOP_ON_FAIL:-1}"
  208. xmakeForceClean="${XMAKE_FORCE_CLEAN:-0}"
  209. unitSyncOnly="${UNIT_SYNC_ONLY:-0}"
  210. # 参数校验
  211. if ! ryanjson_require_01 "UNIT_SKIP_COV" "${unitSkipCov}"; then
  212. return 1
  213. fi
  214. if ! ryanjson_require_01 "UNIT_STOP_ON_FAIL" "${unitStopOnFail}"; then
  215. return 1
  216. fi
  217. if ! ryanjson_require_01 "XMAKE_FORCE_CLEAN" "${xmakeForceClean}"; then
  218. return 1
  219. fi
  220. if ! ryanjson_require_01 "UNIT_SYNC_ONLY" "${unitSyncOnly}"; then
  221. return 1
  222. fi
  223. # 环境依赖检查
  224. ryanjson_require_cmd xmake
  225. # 同步 Unity runner 列表
  226. ryanjson_log_phase "正在校验 Unity runner 列表..."
  227. ryanjson_sync_unity_runner_list
  228. if [[ "${unitSyncOnly}" == "1" ]]; then
  229. ryanjson_log_done "仅完成 Unity runner 列表同步(UNIT_SYNC_ONLY=1)。"
  230. return 0
  231. fi
  232. # 覆盖率输出目录
  233. coverageRoot="localLogs/unitMatrix"
  234. profileRoot="${coverageRoot}/profiles"
  235. ryanjson_prepare_clean_dir "${coverageRoot}"
  236. mkdir -p "${profileRoot}"
  237. # 生成语义矩阵
  238. unitSingleCase="${UNIT_SINGLE_CASE:-}"
  239. if [[ -n "${unitSingleCase}" ]]; then
  240. local normalizedCase=""
  241. if ! normalizedCase="$(ryanjson_parse_single_case "${unitSingleCase}")"; then
  242. return 1
  243. fi
  244. caseList=("${normalizedCase}")
  245. else
  246. if ! caseText="$(ryanjson_emit_semantic_cases "${unitMode}" "UNIT_MODE")"; then
  247. return 1
  248. fi
  249. mapfile -t caseList <<< "${caseText}"
  250. fi
  251. totalCases="${#caseList[@]}"
  252. caseIndex=0
  253. failedCases=0
  254. # 逐用例执行
  255. for entry in "${caseList[@]}"; do
  256. caseIndex=$((caseIndex + 1))
  257. read -r strictKey addAtHead scientific <<< "${entry}"
  258. if ryanjson_unit_run_case "${caseIndex}" "${totalCases}" "${strictKey}" "${addAtHead}" "${scientific}"; then
  259. :
  260. else
  261. failedCases=$((failedCases + 1))
  262. if [[ "${unitStopOnFail}" == "1" ]]; then
  263. echo
  264. ryanjson_log_error "用例 ${caseIndex}/${totalCases} 失败,已按 UNIT_STOP_ON_FAIL=1 提前终止。"
  265. return 1
  266. fi
  267. fi
  268. done
  269. # 汇总覆盖率
  270. if [[ "${unitSkipCov}" != "1" ]]; then
  271. ryanjson_unit_generate_coverage
  272. fi
  273. echo
  274. ryanjson_log_done "单元测试矩阵执行完成。"
  275. ryanjson_log_info "执行模式:${unitMode}"
  276. ryanjson_log_info "总用例数:${totalCases}"
  277. ryanjson_log_info "失败用例数:${failedCases}"
  278. ryanjson_log_info "覆盖率输出目录:${coverageRoot}"
  279. if [[ "${unitSkipCov}" != "1" ]]; then
  280. ryanjson_log_info "覆盖率文本报告:${coverageRoot}/report.txt"
  281. ryanjson_log_info "覆盖率HTML目录:${coverageRoot}/html"
  282. fi
  283. if [[ "${failedCases}" -gt 0 ]]; then
  284. return 1
  285. fi
  286. }
  287. main() {
  288. : "${UNIT_MODE:=full}"
  289. : "${UNIT_SKIP_COV:=1}"
  290. : "${UNIT_STOP_ON_FAIL:=1}"
  291. : "${XMAKE_FORCE_CLEAN:=0}"
  292. : "${UNIT_SYNC_ONLY:=0}"
  293. : "${UNIT_SINGLE_CASE:=}"
  294. ryanjson_print_banner_begin "本地 Base 启动(单元测试矩阵)"
  295. ryanjson_print_banner_kv "UNIT_MODE" "${UNIT_MODE}"
  296. ryanjson_print_banner_kv "UNIT_SKIP_COV" "${UNIT_SKIP_COV}"
  297. ryanjson_print_banner_kv "UNIT_STOP_ON_FAIL" "${UNIT_STOP_ON_FAIL}"
  298. ryanjson_print_banner_kv "XMAKE_FORCE_CLEAN" "${XMAKE_FORCE_CLEAN}"
  299. ryanjson_print_banner_kv "UNIT_SYNC_ONLY" "${UNIT_SYNC_ONLY}"
  300. ryanjson_print_banner_kv_optional "UNIT_SINGLE_CASE" "${UNIT_SINGLE_CASE:-}"
  301. ryanjson_print_banner_end
  302. export UNIT_MODE UNIT_SKIP_COV UNIT_STOP_ON_FAIL XMAKE_FORCE_CLEAN UNIT_SYNC_ONLY UNIT_SINGLE_CASE
  303. ryanjson_unit_main
  304. }
  305. main "$@"