Skip to content

[ci] Testing code coverage workflow #18710

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f052a84
[coverage] Use alma9 to run code coverage
pcanal Apr 22, 2025
151a72a
[coverage] Update yml to match newer build_root.py syntax
pcanal Apr 22, 2025
761ea6b
[coverage] filter out builtins and \*-prefix
pcanal Apr 22, 2025
17d9e18
[coverage] filter out bindings/pyroot/cppyy
pcanal Apr 23, 2025
25abd6a
[coverage] Reduce verbosity of gcovr (more than 77M of debug output)
pcanal Apr 23, 2025
53a6d13
[coverage] Use multiple cores for gcovr
pcanal Apr 23, 2025
7f86f5f
[coverage] Update to v5 of codecov-action
pcanal Apr 23, 2025
48f3d2f
[ci][coverage] Extend env output
pcanal Apr 23, 2025
ca83194
[coverage] Generate artifact for coverage nigthlies
pcanal Apr 24, 2025
d773b27
[ci][coverage] Add S3 authentication.
pcanal Apr 30, 2025
9ebe51e
[coverage] Exclude tests from coverage run.
pcanal May 15, 2025
65dafca
[coverage] Extract part of the (long) gcovr command line
pcanal May 16, 2025
722d607
[coverage] Ignore missing source file (eg. ACLiC)
pcanal May 15, 2025
a55b673
[coverage] Extract from gcovr command line dict related opt
pcanal May 16, 2025
4e33c23
[coverage] Ignore gcovr parse errors.
pcanal May 19, 2025
f406eda
[coverage] Remove existing gcda files
pcanal May 19, 2025
1f9b166
[coverage] Increase suspicious hits threshold to 10,000,000,000
pcanal May 20, 2025
5ab5937
[coverage] Increase suspicious hits threshold to 20,000,000,000.
pcanal May 20, 2025
7ba51e9
[test] Dont rebuild library within test
pcanal May 15, 2025
093ce5f
[NFC] Apply Ruff formatting
pcanal May 21, 2025
8f8a794
[ci] Apply Ruff recommendation.
pcanal May 21, 2025
9b66200
[coverage] Add more options to workflow coverage builds.
pcanal May 22, 2025
dc361a1
[coverage] Allow select incremental for PR coverage build
pcanal May 22, 2025
40f475e
[coverage] Enable test coverage of pull request
pcanal Apr 22, 2025
fd9588b
[coverage] Test PR coverage on explicit request
pcanal May 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/root-ci-config/build_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ def main():
platform_machine = platform.machine()

obj_prefix = f'{args.platform}/{macos_version_prefix}{args.base_ref}/{args.buildtype}_{platform_machine}/{options_hash}'
if args.coverage:
obj_prefix = obj_prefix + "-coverage"

# Make testing of CI in forks not impact artifacts
if 'root-project/root' not in args.repository:
Expand Down Expand Up @@ -126,13 +128,17 @@ def main():
if not WINDOWS:
show_node_state()

if args.coverage and args.incremental:
# Delete all the .gcda files produces by an artefact.
build_utils.remove_file_match_ext(WORKDIR, "gcda")

build(options, args.buildtype)

# Build artifacts should only be uploaded for full builds, and only for
# "official" branches (master, v?-??-??-patches), i.e. not for pull_request
# We also want to upload any successful build, even if it fails testing
# later on.
if not pull_request and not args.incremental and not args.coverage:
if not pull_request and not args.incremental:
archive_and_upload(yyyy_mm_dd, obj_prefix)

if args.binaries:
Expand Down Expand Up @@ -499,9 +505,15 @@ def get_base_head_sha(directory: str, repository: str, merge_sha: str, head_sha:
@github_log_group("Create Test Coverage in XML")
def create_coverage_xml() -> None:
builddir = os.path.join(WORKDIR, "build")
ignore_directories = "runtutorials|interpreter|.*-prefix|bindings/pyroot/cppyy"
ignore_subpattern = "runtutorials|externals|ginclude|googletest-prefix|macosx|winnt|geombuilder|cocoa|quartz|win32gdk|x11|x11ttf|eve|fitpanel|ged|gui|guibuilder|guihtml|qtgsi|qtroot|recorder|sessionviewer|tmvagui|treeviewer|geocad|fitsio|gviz|qt|gviz3d|x3d|spectrum|spectrumpainter|dcache|hdfs|foam|genetic|mlp|quadp|splot|memstat|rpdutils|proof|odbc|llvm|test|interpreter"
ignore_errors = "--gcov-suspicious-hits-threshold=20000000000 --gcov-ignore-errors=source_not_found --gcov-ignore-errors=no_working_dir_found"
exclude_dictionaries = "--exclude='.*/G__.*' --gcov-exclude='.*_ACLiC_dict[.].*' '--exclude=.*_ACLiC_dict[.].*'"
# The output of -v is huge (several 10s of MB at least), we could filter
# the output of -v to keep just the line with ` Processing file:`
result = subprocess_with_log(f"""
cd '{builddir}'
gcovr --output=cobertura-cov.xml --cobertura-pretty --gcov-ignore-errors=no_working_dir_found --merge-mode-functions=merge-use-line-min --exclude-unreachable-branches --exclude-directories="runtutorials|interpreter" --exclude='.*/G__.*' --exclude='.*/(runtutorials|externals|ginclude|googletest-prefix|macosx|winnt|geombuilder|cocoa|quartz|win32gdk|x11|x11ttf|eve|fitpanel|ged|gui|guibuilder|guihtml|qtgsi|qtroot|recorder|sessionviewer|tmvagui|treeviewer|geocad|fitsio|gviz|qt|gviz3d|x3d|spectrum|spectrumpainter|dcache|hdfs|foam|genetic|mlp|quadp|splot|memstat|rpdutils|proof|odbc|llvm|test|interpreter)/.*' --gcov-exclude='.*_ACLiC_dict[.].*' '--exclude=.*_ACLiC_dict[.].*' -v -r ../src ../build
gcovr -j {os.cpu_count()} --output=cobertura-cov.xml --cobertura-pretty {ignore_errors} --merge-mode-functions=merge-use-line-min --exclude-unreachable-branches --exclude-directories="{ignore_directories}" --exclude='.*/({ignore_subpattern})/.*' {exclude_dictionaries} -r ../src ../build
""")

if result != 0:
Expand Down
38 changes: 31 additions & 7 deletions .github/workflows/root-ci-config/build_utils.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
#!/usr/bin/env false

import datetime
import json
import os
import platform
import subprocess
import sys
import textwrap
import datetime
import time
import platform
from functools import wraps
from hashlib import sha1
from http import HTTPStatus
from shutil import which
from typing import Callable, Dict
from collections import namedtuple

from openstack.connection import Connection
from requests import get


def is_macos():
return 'Darwin' == platform.system()

Expand Down Expand Up @@ -218,7 +218,7 @@ def calc_options_hash(options: str) -> str:
"""
options_and_defines = options
if ('march=native' in options):
print_info(f"A march=native build was detected.")
print_info("A march=native build was detected.")
compiler_name = 'c++' if which('c++') else 'clang++'
command = f'echo | {compiler_name} -dM -E - -march=native'
sp_result = subprocess.run([command], shell=True, capture_output=True, text=True)
Expand Down Expand Up @@ -255,12 +255,15 @@ def create_object_local():
try:
create_object_local()
success = True
except:
except Exception:
success = False
sleep_time = sleep_time_unit * attempt
print_warning(f"""Attempt {attempt} to upload {src_file} to {dest_object} failed. Retrying in {sleep_time} seconds...""")
print_warning(
f"""Attempt {attempt} to upload {src_file} to {dest_object} failed. Retrying in {sleep_time} seconds..."""
)
time.sleep(sleep_time)
if success: break
if success:
break

# We try one last time
create_object_local()
Expand Down Expand Up @@ -302,3 +305,24 @@ def download_latest(url: str, prefix: str, destination: str) -> str:
log.add(f"\ncurl --output {destination}/artifacts.tar.gz {url}/{latest}\n")

return f"{destination}/artifacts.tar.gz"


def remove_file_match_ext(directory: str, extension: str) -> str:
"""
Deletes all files in a directory and its subdirectory matching an extension

Args:
directory (str): The path to the directory to search in.
extension (str): The regular expression pattern to match filenames against.
"""
print_fancy(f"Removing gcda files from {directory}")
log.add(f"\nfind {directory} -name \*.gcda -exec rm {{}} \;")
pattern = "." + extension
count = 0
for currentdir, _, files in os.walk(directory):
for filename in files:
if filename.endswith(pattern):
file_path = os.path.join(currentdir, filename)
os.remove(file_path)
count += 1
print_fancy(f"Deleted {count} gcda files")
113 changes: 90 additions & 23 deletions .github/workflows/test-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ on:

# https://github.com/root-project/root/pull/12112#issuecomment-1411004278
# DISABLED: takes 4 hours! Might clang-coverage be a solution?
#pull_request:
# branches:
# - 'master'
# paths-ignore:
# - 'doc/**'
# - 'documentation/**'
pull_request:
branches:
- '**'
paths-ignore:
- 'doc/**'
- 'documentation/**'

workflow_call:
inputs:
Expand All @@ -24,6 +24,9 @@ on:
type: string
required: true
default: master
ref_name:
type: string
default: master

# Enables manual start of workflow
workflow_dispatch:
Expand All @@ -38,14 +41,37 @@ on:
type: string
required: true
default: master
incremental:
description: 'Do incremental build'
type: boolean
required: true
default: true
binaries:
description: Create binary packages and upload them as artifacts
type: boolean
required: true
default: false
buildtype:
description: The CMAKE_BUILD_TYPE to use
type: choice
options:
- Debug
- RelWithDebInfo
- Release
- MinSizeRel
default: Debug
required: true

env:
PYTHONUNBUFFERED: true

jobs:

build-linux:
if: github.repository_owner == 'root-project' || github.event_name == 'pull_request'
if: |
( !(contains(github.event.pull_request.title, '[skip-ci]') || contains(github.event.pull_request.title, '[skip ci]')) )
&& ( ( github.repository_owner == 'root-project' && github.event_name != 'pull_request' )
|| ( github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test coverage') ) )

permissions:
contents: read
Expand All @@ -58,27 +84,62 @@ jobs:

name: Build and test to determine coverage

env:
PYTHONUNBUFFERED: true
OS_APPLICATION_CREDENTIAL_ID: '7f5b64a265244623a3a933308569bdba'
OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
OS_AUTH_TYPE: 'v3applicationcredential'
OS_AUTH_URL: 'https://keystone.cern.ch/v3'
OS_IDENTITY_API_VERSION: 3
OS_INTERFACE: 'public'
OS_REGION_NAME: 'cern'
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
INCREMENTAL: ${{ !contains(github.event.pull_request.labels.*.name, 'clean build') }}

container:
image: registry.cern.ch/root-ci/fedora38:buildready
image: registry.cern.ch/root-ci/alma9:buildready
options: '--security-opt label=disable --rm'
env:
OS_APPLICATION_CREDENTIAL_ID: '7f5b64a265244623a3a933308569bdba'
PYTHONUNBUFFERED: true

steps:
- name: Why did we run
run: |
echo "This workflow was triggered by ${{ github.event_name }}"
echo "The event was ${{ github.event.action }}"
echo "${{ !(contains(github.event.pull_request.title, '[skip-ci]') || contains(github.event.pull_request.title, '[skip ci]')) && ( github.repository_owner == 'root-project' || ( github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test coverage') ) ) }}"
echo "skip-ci: ${{ !(contains(github.event.pull_request.title, '[skip-ci]') || contains(github.event.pull_request.title, '[skip ci]')) }}"
echo "test coverage label: ${{ ( github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'test coverage') ) }}"

- name: Set up Python Virtual Env
# if the `if` expr is false, `if` still has exit code 0.
# if the `if` block is entered, the block's exit code becomes the exit
# code of the `if`.
run: 'if [ -d /py-venv/ROOT-CI/bin/ ]; then . /py-venv/ROOT-CI/bin/activate && echo PATH=$PATH >> $GITHUB_ENV; fi'

# This should be part of the container image
- name: Install cov packages
run: |
sudo dnf -y update
sudo dnf -y install lcov
dnf -y update
dnf -y install lcov
pip3 install gcovr

# This checks out the merge commit if this is a PR
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref_name }}

- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo "$GITHUB_CONTEXT"
JOB_CONTEXT: ${{ toJSON(job) }}
ENV_CONTEXT: ${{ toJSON(env) }}
run: |
echo "$GITHUB_CONTEXT"
echo "--------------------------"
echo "$JOB_CONTEXT"
echo "--------------------------"
echo "$ENV_CONTEXT"

- name: Print debug info
run: 'printf "%s@%s\\n" "$(whoami)" "$(hostname)";
Expand All @@ -87,8 +148,8 @@ jobs:

- name: Apply option override from matrix for this job
env:
OVERRIDE: "coverage=On"
FILE: .github/workflows/root-ci-config/buildconfig/fedora38.txt
OVERRIDE: "coverage=ON"
FILE: .github/workflows/root-ci-config/buildconfig/alma9.txt
shell: bash
run: |
set -x
Expand All @@ -103,40 +164,46 @@ jobs:
GITHUB_PR_ORIGIN: ${{ github.event.pull_request.head.repo.clone_url }}
run: ".github/workflows/root-ci-config/build_root.py
--buildtype Debug
--platform fedora38
--incremental false
--platform alma9
--incremental $INCREMENTAL
--coverage true
--base_ref ${{ github.base_ref }}
--sha ${{ github.sha }}
--head_ref refs/pull/${{ github.event.pull_request.number }}/head:${{ github.event.pull_request.head.ref }}
--head_sha ${{ github.event.pull_request.head.sha }}
--repository ${{ github.server_url }}/${{ github.repository }}
"

- name: Workflow dispatch
if: github.event_name == 'workflow_dispatch'
run: ".github/workflows/root-ci-config/build_root.py
--buildtype Debug
--platform fedora38
--incremental false
--binaries ${{ inputs.binaries }}
--buildtype ${{ inputs.buildtype }}
--platform alma9
--incremental {{ inputs.incremental }}
--coverage true
--base_ref ${{ inputs.base_ref }}
--sha ${{ github.sha }}
--head_ref ${{ inputs.head_ref }}
--head_sha ${{ inputs.head_ref }}
--repository ${{ github.server_url }}/${{ github.repository }}
"

- name: Nightly build
if: github.event_name == 'schedule'
run: ".github/workflows/root-ci-config/build_root.py
--buildtype Debug
--platform fedora38
--platform alma9
--incremental false
--coverage true
--base_ref ${{ github.ref_name }}
--sha ${{ github.sha }}
--repository ${{ github.server_url }}/${{ github.repository }}
"

- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
env_vars: OS,PYTHON
fail_ci_if_error: true
Expand Down
7 changes: 5 additions & 2 deletions roottest/cling/functionTemplate/testcint.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from __future__ import print_function
import ROOT

from sys import stdout

import ROOT


def printme(o):
print("t now %g %d %d" % (o.get["double"](), o.get["int"](), o.get["float"]()))
stdout.flush()

ROOT.gROOT.ProcessLine(".L t.h++")
ROOT.gROOT.ProcessLine(".L t.h+")
sortedMethods = [ item for item in ROOT.t.__dict__.keys() if item[0:2] != '__' ]
sortedMethods.sort()
print("# just a comment")
Expand Down
10 changes: 6 additions & 4 deletions roottest/root/aclic/load/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ ROOTTEST_ADD_TEST(autoload
OUTREF execautoload.ref
DEPENDS autoloadtest.C)

ROOTTEST_ADD_TEST(reload
COPY_TO_BUILDDIR Reload.root
MACRO assertReload.C
OUTREF assertReload.ref)
if(NOT coverage)
ROOTTEST_ADD_TEST(reload
COPY_TO_BUILDDIR Reload.root
MACRO assertReload.C
OUTREF assertReload.ref)
endif()

ROOTTEST_ADD_TEST(linktest
MACRO execlinktest.C
Expand Down
10 changes: 6 additions & 4 deletions roottest/root/meta/tclass/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,12 @@ ROOTTEST_ADD_TEST(execInitOrder
OUTREF execInitOrder.ref
FIXTURES_REQUIRED InitOrder)

ROOTTEST_ADD_TEST(tclassStl
MACRO tclassStl.cxx+
OUTREF tclassStl.ref
RUN_SERIAL)
if(NOT coverage)
ROOTTEST_ADD_TEST(tclassStl
MACRO tclassStl.cxx+
OUTREF tclassStl.ref
RUN_SERIAL)
endif()

ROOTTEST_ADD_TEST(TClassAtTearDown
MACRO execTClassAtTearDown.C
Expand Down
Loading