From 42cdd2fdfec338ec8849074b7fa57a2df4955710 Mon Sep 17 00:00:00 2001 From: zhanghongyuan Date: Thu, 13 Aug 2026 17:20:15 +0800 Subject: [PATCH] feat(test): add ut-summary.json generation for test results and coverage - Add gen-ut-summary.py: standalone script to parse gtest XML and lcov coverage data, output structured JSON (test_cases/line_coverage/function_coverage) - Update test-prj-running.sh: export env vars, set +e for test running, call gen-ut-summary.py, propagate test exit code --- tests/gen-ut-summary.py | 127 ++++++++++++++++++++++++++++++++++++++ tests/test-prj-running.sh | 30 +++++---- 2 files changed, 146 insertions(+), 11 deletions(-) create mode 100755 tests/gen-ut-summary.py diff --git a/tests/gen-ut-summary.py b/tests/gen-ut-summary.py new file mode 100755 index 00000000..6ccee178 --- /dev/null +++ b/tests/gen-ut-summary.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Generate ut-summary.json from gtest XML reports and lcov coverage data. + +Usage: + Called from shell test scripts. Reads environment variables: + - projectdir: project root directory + - builddir: build directory name (relative to projectdir) + - reportdir: report output directory name (relative to projectdir) + + Optional overrides (take precedence over defaults): + - GTEST_XML_DIR: directory containing gtest XML reports + - COVERAGE_INFO: path to lcov .info file for coverage parsing + + Output: {projectdir}/{reportdir}/ut-summary.json +""" + +import json +import xml.etree.ElementTree as ET +import glob +import os +import subprocess +import re +import sys + + +def parse_gtest_xml(xml_dir): + """Parse Google Test / JUnit XML output and return (total, passed, failed).""" + total = passed = failed = 0 + for xml_file in sorted(glob.glob(os.path.join(xml_dir, "*.xml"))): + try: + root = ET.parse(xml_file).getroot() + # JUnit format uses as root with aggregated counts + # gtest XML uses or as root + t = int(root.get("tests", 0)) + f_count = int(root.get("failures", 0)) + err_count = int(root.get("errors", 0)) + total += t + failed += f_count + err_count + passed += t - f_count - err_count + except Exception as e: + print(f"Warning: failed to parse {xml_file}: {e}", file=sys.stderr) + return total, passed, failed + + +def parse_lcov_summary(coverage_info): + """Parse lcov --summary output and return coverage dict.""" + result = {} + if not os.path.exists(coverage_info): + print(f"Warning: coverage info file not found: {coverage_info}", file=sys.stderr) + return result + + lcov_out = subprocess.run( + ["lcov", "--summary", coverage_info, "--rc", "lcov_branch_coverage=1"], + capture_output=True, text=True + ) + summary_text = lcov_out.stdout + lcov_out.stderr + + # Parse lines: "lines......: XX.X% (NNN of MMMM lines)" + m_lines = re.search(r'lines.*?:\s*([\d.]+)%\s*\((\d+)\s+of\s+(\d+)\s+\w+\)', summary_text) + if m_lines: + pct, hit, total = m_lines.groups() + result["line_coverage"] = { + "total": int(total), + "passed": int(hit), + "failed": int(total) - int(hit), + "coverage": f"{float(pct):.2f}%" + } + + # Parse functions: "functions..: XX.X% (NNN of MMMM functions)" + m_func = re.search(r'functions.*?:\s*([\d.]+)%\s*\((\d+)\s+of\s+(\d+)\s+\w+\)', summary_text) + if m_func: + pct, hit, total = m_func.groups() + result["function_coverage"] = { + "total": int(total), + "passed": int(hit), + "failed": int(total) - int(hit), + "coverage": f"{float(pct):.2f}%" + } + + return result + + +def main(): + projectdir = os.environ.get("projectdir") + builddir = os.environ.get("builddir") + reportdir = os.environ.get("reportdir") + + if not all([projectdir, builddir, reportdir]): + print("Error: environment variables projectdir, builddir, reportdir are required", file=sys.stderr) + sys.exit(1) + + # Resolve paths (env overrides take precedence) + gtest_dir = os.environ.get("GTEST_XML_DIR", + os.path.join(projectdir, builddir, "report")) + coverage_info = os.environ.get("COVERAGE_INFO", + os.path.join(projectdir, builddir, "coverage.info")) + + # --- Test case counts from gtest XML --- + total, passed, failed = parse_gtest_xml(gtest_dir) + + result = { + "test_cases": { + "total": total, + "passed": passed, + "failed": failed + } + } + + # --- Coverage from lcov --summary --- + coverage_data = parse_lcov_summary(coverage_info) + result.update(coverage_data) + + # --- Write output --- + output_path = os.path.join(projectdir, reportdir, "ut-summary.json") + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, "w") as f: + json.dump(result, f, indent=2) + + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tests/test-prj-running.sh b/tests/test-prj-running.sh index 17ac8bb0..4cdd8ee5 100644 --- a/tests/test-prj-running.sh +++ b/tests/test-prj-running.sh @@ -6,15 +6,13 @@ set -u -builddir=build -reportdir=build-ut +export builddir=build +export reportdir=build-ut +export scriptdir="$(cd "$(dirname "$0")" && pwd)" +export projectdir="$(cd "${scriptdir}/.." && pwd)" -# Resolve project root (parent of the tests/ directory that holds this script) -script_dir="$(cd "$(dirname "$0")" && pwd)" -project_root="$(cd "${script_dir}/.." && pwd)" - -build_path="${project_root}/${builddir}" -report_path="${project_root}/${reportdir}" +build_path="${projectdir}/${builddir}" +report_path="${projectdir}/${reportdir}" # Fresh build directory to ensure a clean coverage run rm -rf "${build_path}" @@ -32,7 +30,7 @@ cmake -DCMAKE_SAFETYTEST_ARG="CMAKE_SAFETYTEST_ARG_ON" \ -DBUILD_TESTS=ON \ -DUSE_PDFIUM_BUNDLE=ON \ -DCMAKE_BUILD_TYPE=Debug \ - "${project_root}" + "${projectdir}" # Compile tests target make -j"$(nproc)" test-deepin-reader @@ -41,7 +39,10 @@ make -j"$(nproc)" test-deepin-reader mkdir -p "${build_path}/report" # Run tests and produce XML report +set +e ./tests/test-deepin-reader --gtest_output=xml:"${build_path}/report/report_deepin-reader.xml" +test_exit_code=$? +set -e # Directory that holds coverage artifacts (build tree of the project) workdir="${build_path}" @@ -50,7 +51,9 @@ workdir="${build_path}" lcov --directory "${workdir}" --zerocounters || true # Re-run tests so .gcda files reflect a clean run +set +e ./tests/test-deepin-reader --gtest_output=xml:"${build_path}/report/report_deepin-reader.xml" +set -e # Collect coverage data lcov -d "${workdir}" -c -o ./coverage.info @@ -60,7 +63,7 @@ lcov --extract ./coverage.info '*/reader/*' -o ./coverage.info lcov --remove ./coverage.info '*/tests/*' -o ./coverage.info # Exclude compiler-generated and unreachable functions (D0Ev, Q_OBJECT tr, env-dependent lambdas) -python3 "${script_dir}/exclude_unreachable.py" ./coverage.info ./coverage.info +python3 "${scriptdir}/exclude_unreachable.py" ./coverage.info ./coverage.info # Generate HTML report genhtml -o ./html ./coverage.info @@ -73,4 +76,9 @@ cp -r html "${report_path}/" cp -r "${build_path}/report" "${report_path}/" cp -r asan*.log* "${report_path}/asan_deepin-reader.log" 2>/dev/null || true -exit 0 +# 生成摘要 JSON +echo "==> Generating summary JSON: ${report_path}/ut-summary.json" + +python3 "${scriptdir}/gen-ut-summary.py" + +exit $test_exit_code