[rocprof-compute] Automate ctest coverage and test cases on runners with CDash (#1481)

* Add nightly coverage workflow

* ruff formatting

* temp workflow testing

* restore workflow file

* add workflow condition

* update workflow file

* update workflow file

* fix typo in run-ci.py

* edit run-ci.py

* add python deps install

* add python deps install

* add python deps install

* add python deps install

* check if enable coverage is on when using workflow

* remove github CI breakdown and fix enable coverage

* set cache variables must be set before dashboard starts

* Update run-ci.py

* Update run-ci.py to fix ctest cache

* Update rocprofiler-compute-code-coverage.yml to install tests

* Update rocprofiler-compute-code-coverage.yml

* Restore workflow file

* Update run-ci.py

* Simplify workflow build command

* Update run-ci.py to build tests

* edited run-ci script

* edit ctest configure commands

* edit ctest configure commands to be on one line

* edit ctest configure command to include path to amdclang++

* update clang check in tests/cmakelists.txt

* update rocm

* update rocm

* update rocm version 7.0.2

* update tests/CMakeLists.txt

* use tarball instead for rocm install

* apt install rocm-dev instead for 7.0.0 release

* workflow tweaks

* update to use new 'tools' dir

* install rocm-dev

* add CMAKE_CXX_COMPILER as clang

* update tests/cmakelists.txt

* update cdasg site and build names

* remove run automatically on pull requests

* ruff format

* increased timeouts for tests

* add back reruns for workflow testing

* fix typo

* rename workflow "nightly" -> "code"

* added tracks to keep track of gpu (325 vs 355)

* remove test_db_connector.py

* revert build names and tracking

* update workflow pushes

* CMake format

* changed parallel level back to 1
이 커밋은 다음에 포함됨:
jamessiddeley-amd
2025-11-17 09:24:24 -05:00
커밋한 사람 GitHub
부모 75ad45d5f1
커밋 d49e2e35fd
8개의 변경된 파일392개의 추가작업 그리고 5996개의 파일을 삭제
+8 -4
파일 보기
@@ -164,7 +164,7 @@ if(LOCALHOST MATCHES "TheraS01|.*\.thera\.amd\.com|thera-hn")
file(READ ${PROJECT_SOURCE_DIR}/cmake/modfile.thera.mod mod_additions)
file(
APPEND
${PROJECT_BINARY_DIR}/${MOD_INSTALL_PATH}/${ROCPROFCOMPUTE_FULL_VERSION}.lua
${PROJECT_BINARY_DIR}/${MOD_INSTALL_PATH}/${ROCPROFCOMPUTE_FULL_VERSION}.lua
${mod_additions}
)
list(POP_BACK CMAKE_MESSAGE_INDENT)
@@ -336,7 +336,11 @@ set_tests_properties(
test_profile_misc
test_profile_path
test_profile_roofline
PROPERTIES LABELS "profile" RESOURCE_GROUPS gpus:1
test_profile_section
test_profile_pc_sampling
test_profile_sets_func
test_profile_live_attach_detach
PROPERTIES LABELS "profile" RESOURCE_GROUPS gpus:1 TIMEOUT 1800
)
# ---------------------------
@@ -463,6 +467,7 @@ if(${ENABLE_COVERAGE})
DEPENDS "${ALL_TESTS_STRING}"
LABELS "coverage"
ENVIRONMENT "COVERAGE_FILE=${PROJECT_SOURCE_DIR}/.coverage"
TIMEOUT 600
)
endif()
@@ -489,8 +494,7 @@ install(
src/config.py
src/rocprof_compute_base.py
src/roofline.py
VERSION
VERSION.sha
VERSION VERSION.sha
DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/${PROJECT_NAME}
COMPONENT main
)
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Diff 로드
-77
파일 보기
@@ -1,77 +0,0 @@
# Coverage Workflow for rocprofiler-compute in Monorepo
## Overview
This document describes the coverage generation and CDash upload workflow for `rocprofiler-compute` within the super-repo structure.
## Directory Structure
```
monorepo/
├── projects/
│ └── rocprofiler-compute/
│ ├── CMakeLists.txt
│ ├── coverage/
│ │ └── coverage-latest.xml # committed coverage file
│ ├── tools/
│ │ ├── update_coverage.sh # coverage generation/update script
│ │ └── run-ci.py # CDash upload script
│ └── ...
└── .github/
└── workflows/
└── rocprofiler-compute-cdash.yml # gitHub Action for auto-upload
```
## Workflows
### 1. Manual Coverage Generation (Periodic Update)
Run this periodically to update the coverage baseline:
```bash
# From monorepo root
cd projects/rocprofiler-compute
./tools/update_coverage.sh
# This will:
# - Build with coverage enabled
# - Run all tests
# - Generate coverage.xml
# - Copy/overwrite to coverage/coverage-latest.xml
# - Show instructions for committing
```
### 2. Commit Coverage to Repository
```bash
# From monorepo root
git add projects/rocprofiler-compute/coverage/coverage-latest.xml
git commit -m "rocprofiler-compute: Update coverage to XX.X% line coverage"
git push origin develop
```
### 3. Automatic CDash Upload
When `coverage/coverage-latest.xml` is pushed to `develop` branch:
- GitHub Action triggers automatically
- Uploads coverage to CDash
- Posts summary to GitHub Actions
## CDash Project
View results at: https://my.cdash.org/index.php?project=rocprofiler-compute
## Maintenance
## Troubleshooting
### Coverage Generation Fails
```bash
#check Python dependencies
pip install coverage pytest pytest-cov
#verify tests can run
cd projects/rocprofiler-compute/build
ctest --verbose
```
+2 -2
파일 보기
@@ -1,10 +1,10 @@
set(CMAKE_HIP_COMPILER "amdclang++" CACHE STRING "desired c++ compiler" FORCE)
if(CMAKE_HIP_COMPILER_ID STREQUAL "Clang")
if(CMAKE_HIP_COMPILER_ID STREQUAL "Clang" OR CMAKE_HIP_COMPILER_ID STREQUAL "ROCMClang")
message(STATUS "Using ${CMAKE_HIP_COMPILER} to build for amdgpu backend")
else()
message(
FATAL_ERROR
"'amdclang++' compiler required to compile test binaries for ROCm platform."
"'amdclang++' compiler required to compile test binaries for ROCm platform. Found: ${CMAKE_HIP_COMPILER_ID}"
)
endif()
+225 -155
파일 보기
@@ -23,13 +23,8 @@
# THE SOFTWARE.
##############################################################################
"""
CI script to upload coverage XML files to CDash
"""
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
@@ -60,16 +55,44 @@ def detect_repo_structure():
return False, None, cwd
def create_ctest_script(args):
"""Generate a CTest script for uploading coverage data"""
def generate_ctest_dashboard_script(args, source_dir, binary_dir):
"""
Generate a complete CTest dashboard script that handles:
- Configuration
- Build
- Test
- Coverage
- Submit to CDash
"""
cache_entries = [
"CMAKE_BUILD_TYPE:STRING=Release",
f"CMAKE_PREFIX_PATH:PATH={os.environ.get('ROCM_PATH', '/opt/rocm')}",
"ENABLE_TESTS:BOOL=ON",
"INSTALL_TESTS:BOOL=ON",
"ENABLE_COVERAGE:BOOL=ON",
f"PYTEST_NUMPROCS:STRING={args.pytest_numprocs}",
]
test_args = ""
if args.ctest_args:
test_args = " ".join(args.ctest_args)
# ruff: noqa
script_content = f"""
cmake_minimum_required(VERSION 3.19)
##############################################################################
# CTest Dashboard Script for rocprofiler-compute
# Auto-generated by run-ci.py
##############################################################################
# Dashboard configuration
set(CTEST_PROJECT_NAME "{args.project_name}")
set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC")
set(CTEST_DROP_SITE_CDASH TRUE)
# CDash submission settings
if(CMAKE_VERSION VERSION_GREATER 3.14)
set(CTEST_SUBMIT_URL "{args.submit_url}")
else()
@@ -78,93 +101,143 @@ else()
set(CTEST_DROP_LOCATION "{args.submit_path}")
endif()
# Build identification
set(CTEST_SITE "{args.site}")
set(CTEST_BUILD_NAME "{args.build_name}")
set(CTEST_SOURCE_DIRECTORY "{args.source_dir}")
set(CTEST_BINARY_DIRECTORY "{args.binary_dir}")
set(CTEST_SOURCE_DIRECTORY "{source_dir}")
set(CTEST_BINARY_DIRECTORY "{binary_dir}")
file(MAKE_DIRECTORY "${{CTEST_BINARY_DIRECTORY}}")
# Build config
set(CTEST_CMAKE_GENERATOR "Unix Makefiles")
set(CTEST_BUILD_CONFIGURATION "Release")
if(NOT EXISTS "${{CTEST_BINARY_DIRECTORY}}/CMakeCache.txt")
file(WRITE "${{CTEST_BINARY_DIRECTORY}}/CMakeCache.txt"
"CMAKE_PROJECT_NAME:STATIC={args.project_name}\\n")
endif()
# Config CMake command with all required options
set(CTEST_CONFIGURE_COMMAND "cmake -B ${{CTEST_BINARY_DIRECTORY}} ${{CTEST_SOURCE_DIRECTORY}} -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH={os.environ.get("ROCM_PATH", "/opt/rocm")} -DENABLE_TESTS=ON -DINSTALL_TESTS=ON -DENABLE_COVERAGE=ON -DPYTEST_NUMPROCS={args.pytest_numprocs}")
file(WRITE "${{CTEST_BINARY_DIRECTORY}}/CTestConfig.cmake" "
set(CTEST_PROJECT_NAME \\"{args.project_name}\\")
set(CTEST_NIGHTLY_START_TIME \\"01:00:00 UTC\\")
if(CMAKE_VERSION VERSION_GREATER 3.14)
set(CTEST_SUBMIT_URL \\"{args.submit_url}\\")
else()
set(CTEST_DROP_METHOD \\"https\\")
set(CTEST_DROP_SITE \\"{args.cdash_host}\\")
set(CTEST_DROP_LOCATION \\"{args.submit_path}\\")
endif()
set(CTEST_DROP_SITE_CDASH TRUE)
")
message(STATUS "CMake configure command: ${{CTEST_CONFIGURE_COMMAND}}")
message(STATUS "Starting {args.mode} dashboard...")
ctest_start({args.mode})
set(COVERAGE_SRC_FILE "{args.coverage_file}")
set(COVERAGE_DEST_FILE "${{CTEST_BINARY_DIRECTORY}}/coverage.xml")
# Config step
message(STATUS "Configuring project...")
ctest_configure(
RETURN_VALUE configure_result
CAPTURE_CMAKE_ERROR configure_error
)
if(NOT EXISTS "${{COVERAGE_SRC_FILE}}")
message(FATAL_ERROR "Coverage file not found: ${{COVERAGE_SRC_FILE}}")
if(configure_result OR configure_error)
message(WARNING "Configuration had issues but continuing...")
endif()
file(COPY "${{COVERAGE_SRC_FILE}}" DESTINATION "${{CTEST_BINARY_DIRECTORY}}")
get_filename_component(SRC_FILENAME "${{COVERAGE_SRC_FILE}}" NAME)
if(NOT SRC_FILENAME STREQUAL "coverage.xml")
file(RENAME "${{CTEST_BINARY_DIRECTORY}}/${{SRC_FILENAME}}"
"${{COVERAGE_DEST_FILE}}")
# Build step
message(STATUS "Building project...")
set(CTEST_BUILD_FLAGS "-j{args.build_jobs}")
ctest_build(
TARGET {"install" if args.install else "all"}
RETURN_VALUE build_result
CAPTURE_CMAKE_ERROR build_error
)
if(build_result OR build_error)
message(WARNING "Build had issues but continuing...")
endif()
message(STATUS "Processing coverage file: ${{COVERAGE_DEST_FILE}}")
# Test step
message(STATUS "Running tests...")
ctest_test(
{test_args}
PARALLEL_LEVEL 1
RETURN_VALUE test_result
CAPTURE_CMAKE_ERROR test_error
)
file(MAKE_DIRECTORY "${{CTEST_BINARY_DIRECTORY}}/Testing")
file(MAKE_DIRECTORY "${{CTEST_BINARY_DIRECTORY}}/Testing/CoverageInfo")
file(COPY "${{COVERAGE_DEST_FILE}}"
DESTINATION "${{CTEST_BINARY_DIRECTORY}}/Testing/CoverageInfo")
ctest_coverage()
ctest_submit(PARTS Coverage
RETRY_COUNT 3
RETRY_DELAY 5
CAPTURE_CMAKE_ERROR submit_error)
if(submit_error)
message(FATAL_ERROR "Failed to submit coverage to CDash. "
"Error code: ${{submit_error}}")
if(test_result OR test_error)
message(WARNING "Some tests failed but continuing with coverage and upload...")
endif()
message(STATUS "Successfully submitted coverage to CDash")
# Coverage step (if coverage file exists)
set(COVERAGE_FILE "${{CTEST_BINARY_DIRECTORY}}/coverage.xml")
if(EXISTS "${{COVERAGE_FILE}}")
message(STATUS "Processing coverage data...")
file(MAKE_DIRECTORY "${{CTEST_BINARY_DIRECTORY}}/Testing/CoverageInfo")
file(COPY "${{COVERAGE_FILE}}"
DESTINATION "${{CTEST_BINARY_DIRECTORY}}/Testing/CoverageInfo/")
ctest_coverage(
RETURN_VALUE coverage_result
CAPTURE_CMAKE_ERROR coverage_error
)
if(coverage_result OR coverage_error)
message(WARNING "Coverage processing had issues but continuing...")
endif()
else()
message(STATUS "No coverage file found at ${{COVERAGE_FILE}}")
message(STATUS "Skipping coverage step")
endif()
message(STATUS "Submitting to CDash...")
ctest_submit(
RETRY_COUNT 3
RETRY_DELAY 5
RETURN_VALUE submit_result
CAPTURE_CMAKE_ERROR submit_error
)
if(submit_result OR submit_error)
message(WARNING "CDash submission encountered issues")
message(STATUS "Results available locally in: ${{CTEST_BINARY_DIRECTORY}}/Testing/")
else()
message(STATUS "Successfully submitted to CDash!")
message(STATUS "View at: {args.submit_url.replace("/submit.php?project=", "/index.php?project=")}")
message(STATUS "Build name: {args.build_name}")
endif()
message(STATUS "")
message(STATUS "========================================")
message(STATUS "Dashboard Summary")
message(STATUS "========================================")
if(test_result)
message(STATUS "Tests: FAILED (some tests failed or timed out)")
else()
message(STATUS "Tests: PASSED")
endif()
if(submit_result OR submit_error)
message(STATUS "CDash Upload: FAILED")
else()
message(STATUS "CDash Upload: SUCCESS")
endif()
message(STATUS "========================================")
# For nightly/continuous, don't fail on test failures
# Only warn in Experimental mode
if("{args.mode}" STREQUAL "Experimental" AND test_result)
message(WARNING "Tests failed in Experimental mode")
endif()
"""
return script_content
def main():
if not os.getenv("CI"):
print("WARNING: CDash upload should normally only be done from CI/CD")
response = input("Continue anyway? (y/N): ")
if response.lower() != "y":
print("Aborted.")
return 1
parser = argparse.ArgumentParser(
description="Upload coverage XML to CDash for rocprofiler-compute"
description="CI script for rocprofiler-compute: build, test, and upload to CDash"
)
# required
parser.add_argument(
"--coverage-file", required=True, help="Path to coverage XML file"
"--build-name",
"--name",
dest="build_name",
required=True,
help="Build name for CDash",
)
parser.add_argument("--build-name", required=True, help="Build name for CDash")
# optional
parser.add_argument(
"--project-name", default="rocprofiler-compute", help="CDash project name"
"--project-name",
default="rocprofiler-compute",
help="CDash project name",
)
parser.add_argument(
"--submit-url",
@@ -172,7 +245,9 @@ def main():
help="CDash submission URL",
)
parser.add_argument(
"--cdash-host", default="my.cdash.org", help="CDash host (for older CMake)"
"--cdash-host",
default="my.cdash.org",
help="CDash host (for older CMake)",
)
parser.add_argument(
"--submit-path",
@@ -186,11 +261,13 @@ def main():
)
parser.add_argument(
"--source-dir",
"-S",
default=None,
help="Source directory path (auto-detected if not provided)",
)
parser.add_argument(
"--binary-dir",
"-B",
default=None,
help="Binary directory path (defaults to source-dir/build)",
)
@@ -201,10 +278,33 @@ def main():
help="CTest dashboard mode",
)
parser.add_argument(
"--dry-run", action="store_true", help="Generate script but don't execute"
"--build-jobs",
"-j",
type=int,
default=8,
help="Number of parallel build jobs",
)
parser.add_argument(
"--install",
action="store_true",
help="Build install target instead of all target",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Generate dashboard script but don't execute",
)
parser.add_argument(
"--pytest-numprocs",
type=int,
default=4,
help="Number of parallel processes for pytest",
)
args = parser.parse_args()
args, unknown = parser.parse_known_args()
args.cmake_args = []
args.ctest_args = []
is_monorepo, monorepo_root, project_root = detect_repo_structure()
@@ -220,113 +320,83 @@ def main():
else:
args.site = os.uname().nodename
coverage_path = Path(args.coverage_file)
if not coverage_path.is_absolute():
if not coverage_path.exists():
coverage_path = project_root / args.coverage_file
if not coverage_path.exists():
print(f"ERROR: Coverage file not found: {coverage_path}", file=sys.stderr)
print(f" Searched in: {Path(args.coverage_file).absolute()}")
print(f" And in: {project_root / args.coverage_file}")
return 1
try:
import xml.etree.ElementTree as ET
tree = ET.parse(coverage_path)
root = tree.getroot()
if root.tag != "coverage":
print(
f"ERROR: File does not appear to be a coverage XML file "
f"(root tag: {root.tag})",
file=sys.stderr,
)
return 1
line_rate = float(root.get("line-rate", 0)) * 100
print(f"Line Coverage: {line_rate:.1f}%")
except Exception as e:
print(f"ERROR: Could not parse coverage XML file: {e}", file=sys.stderr)
return 1
args.coverage_file = str(coverage_path.absolute())
args.source_dir = str(Path(args.source_dir).absolute())
args.binary_dir = str(Path(args.binary_dir).absolute())
source_dir = Path(args.source_dir).absolute()
binary_dir = Path(args.binary_dir).absolute()
print("=" * 80)
print("rocprofiler-compute CI Dashboard")
print("=" * 80)
print(f"Repository type: {'Monorepo' if is_monorepo else 'Standalone'}")
print(f"Project root: {project_root}")
print(f"Uploading coverage file: {args.coverage_file}")
print(f"Source directory: {source_dir}")
print(f"Binary directory: {binary_dir}")
print(f"Build name: {args.build_name}")
print(f"Project: {args.project_name}")
print(f"Dashboard mode: {args.mode}")
print(f"CDash project: {args.project_name}")
print(f"Site: {args.site}")
print(f"Submit URL: {args.submit_url}")
print("=" * 80)
# Ensure build directory exists
os.makedirs(args.binary_dir, exist_ok=True)
# Copy CTestConfig.cmake if it exists in source
source_ctest_config = Path(args.source_dir) / "CTestConfig.cmake"
if source_ctest_config.exists():
shutil.copy2(source_ctest_config, Path(args.binary_dir) / "CTestConfig.cmake")
print("Copied CTestConfig.cmake to build directory")
script_content = create_ctest_script(args)
script_content = generate_ctest_dashboard_script(args, source_dir, binary_dir)
if args.dry_run:
print("\nGenerated CTest script:")
print("=" * 50)
print("\nGenerated CTest Dashboard Script:")
print("=" * 80)
print(script_content)
print("=" * 80)
return 0
binary_dir.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".cmake", delete=False, dir=binary_dir
) as f:
f.write(script_content)
script_path = f.name
print(f"\nDashboard script: {script_path}")
print("=" * 80)
try:
with tempfile.NamedTemporaryFile(mode="w", suffix=".cmake", delete=False) as f:
f.write(script_content)
script_path = f.name
print(f"\nScript written to: {script_path}")
original_dir = os.getcwd()
os.chdir(args.source_dir)
cmd = ["ctest", "-S", script_path, "-V"]
print(f"Executing: {' '.join(cmd)}")
print(f"Working directory: {os.getcwd()}")
print(f"Running: {' '.join(cmd)}")
print("=" * 80)
print()
result = subprocess.run(cmd, capture_output=True, text=True)
if result.stdout:
print("\nSTDOUT:")
print(result.stdout)
if result.stderr:
print("\nSTDERR:")
print(result.stderr)
os.chdir(original_dir)
if result.returncode != 0:
print(f"\nCTest failed with return code: {result.returncode}")
return result.returncode
print("\n✅ Coverage successfully uploaded to CDash!")
cdash_url = args.submit_url.replace(
"/submit.php?project=", "/index.php?project="
process = subprocess.Popen(
cmd,
cwd=source_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True,
)
print(f"View results at: {cdash_url}")
return 0
for line in process.stdout:
print(line, end="")
returncode = process.wait()
print()
print("=" * 80)
if returncode == 0:
print("✅ Dashboard completed successfully!")
else:
print(f"⚠️ Dashboard completed with return code: {returncode}")
print("=" * 80)
return returncode
except Exception as e:
print(f"Error executing CTest script: {e}", file=sys.stderr)
print(f"\nError executing dashboard script: {e}", file=sys.stderr)
import traceback
traceback.print_exc()
return 1
finally:
if "script_path" in locals():
try:
os.unlink(script_path)
except Exception:
pass
try:
os.unlink(script_path)
except Exception:
pass
if __name__ == "__main__":
-81
파일 보기
@@ -1,81 +0,0 @@
#!/bin/bash
##############################################################################
# MIT License
#
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
##############################################################################
# generate_coverage.sh
set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="$PROJECT_ROOT/build"
COVERAGE_FILE="$PROJECT_ROOT/coverage/Coverage.xml"
echo "=== Generating Coverage for Mainline Promotion ==="
echo "Setting up clean build..."
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
echo "Configuring with coverage enabled..."
cmake -DENABLE_COVERAGE=ON -DENABLE_TESTS=ON -DPYTEST_NUMPROCS=8 ..
echo "Building..."
make -j$(nproc)
echo "Running ALL tests to generate coverage (this may take a while)..."
ctest --verbose --output-on-failure --parallel 1 || {
echo "WARNING: Some tests failed, but continuing with coverage generation"
}
echo "Generating coverage report..."
ctest -R generate_coverage_report --output-on-failure --parallel 2
if [ ! -f "coverage.xml" ]; then
echo "ERROR: coverage.xml not generated"
exit 1
fi
COVERAGE_INFO=$(python3 -c "
import xml.etree.ElementTree as ET
tree = ET.parse('coverage.xml')
root = tree.getroot()
line_rate = float(root.get('line-rate', 0)) * 100
print(f'{line_rate:.1f}%')
")
mkdir -p "$PROJECT_ROOT/coverage"
cp coverage.xml "$COVERAGE_FILE"
echo ""
echo "=== Coverage Generated Successfully ==="
echo "Line Coverage: $COVERAGE_INFO"
echo "File: $COVERAGE_FILE"
echo ""
echo "Next steps:"
echo "1. git add $COVERAGE_FILE"
echo "2. git commit -m 'Update coverage: $COVERAGE_INFO'"
echo "3. Proceed with push to develop"
echo "4. CDash upload will happen automatically on push"
echo ""