Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
106 changes: 65 additions & 41 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ env:
COMMIT: ${{ inputs.release_commit }}
FREEBSD_CLANG_VERSION: 19
FREEBSD_VERSION: '15.1'
OPENCPPCOVERAGE_VERSION: '0.9.9.0'
PYTHON_VERSION: ${{ inputs.python_version }}

jobs:
Expand Down Expand Up @@ -225,20 +224,18 @@ jobs:
mingw-w64-${{ matrix.toolchain }}-ninja
mingw-w64-${{ matrix.toolchain }}-toolchain

- name: Setup Dependencies Windows MSVC
- name: Locate MSVC coverage collector
if: matrix.kind == 'msvc'
run: |
choco install opencppcoverage --version=${{ env.OPENCPPCOVERAGE_VERSION }} --yes --no-progress

$openCppCoverageDir = "${env:ProgramFiles}\OpenCppCoverage"
if (!(Test-Path (Join-Path $openCppCoverageDir "OpenCppCoverage.exe"))) {
$openCppCoverageDir = "${env:ProgramFiles(x86)}\OpenCppCoverage"
}
if (!(Test-Path (Join-Path $openCppCoverageDir "OpenCppCoverage.exe"))) {
throw "OpenCppCoverage.exe was not found after Chocolatey install."
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
$collector = & $vswhere -latest -products '*' `
-find 'Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\Microsoft.CodeCoverage.Console.exe' |
Select-Object -First 1
if (!$collector) {
throw "Microsoft.CodeCoverage.Console.exe was not found in the latest Visual Studio installation."
}

$openCppCoverageDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
"MSVC_COVERAGE_COLLECTOR=$collector" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

- name: Setup Windows coverage inspection tools
if: matrix.kind == 'msvc'
Expand Down Expand Up @@ -434,30 +431,48 @@ jobs:
id: test_msvc
if: matrix.kind == 'msvc'
run: |
$openCppCoverage = (Get-Command OpenCppCoverage.exe -ErrorAction SilentlyContinue).Source
if (!$openCppCoverage) {
$candidates = @(
"${env:ProgramFiles}\OpenCppCoverage\OpenCppCoverage.exe",
"${env:ProgramFiles(x86)}\OpenCppCoverage\OpenCppCoverage.exe"
)
$openCppCoverage = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1
$testBinary = Join-Path $env:GITHUB_WORKSPACE `
"cmake-build-ci\tests\$env:CMAKE_BUILD_CONFIG\test_libvirtualhid.exe"
$reportRoot = Join-Path $env:GITHUB_WORKSPACE "cmake-build-ci\reports"
$coveragePath = Join-Path $reportRoot "coverage.xml"
$junitPath = Join-Path $reportRoot "junit.xml"
$settingsPath = Join-Path $reportRoot "coverage.config"
[xml] $settings = Get-Content -LiteralPath tests/msvc-coverage.config.in -Raw
$settings.Configuration.CodeCoverage.ModulePaths.IncludeDirectories.Directory.InnerText =
Split-Path $testBinary
$settings.Configuration.CodeCoverage.ModulePaths.Include.ModulePath =
'^' + [regex]::Escape($testBinary) + '$'
$sourcePattern = [regex]::Escape($env:GITHUB_WORKSPACE).Replace('\\', '[\\/]')
$settings.Configuration.CodeCoverage.Sources.Include.Source = "$sourcePattern[\\/](examples|src|tools)[\\/].*"
# The broker test hook compiles a private copy only for failure injection.
$settings.Configuration.CodeCoverage.Sources.Exclude.Source =
"$sourcePattern[\\/]src[\\/]platform[\\/]windows[\\/]broker[\\/].*"
$settings.Save($settingsPath)
Remove-Item -LiteralPath $coveragePath, $junitPath -Force -ErrorAction SilentlyContinue
Push-Location -LiteralPath (Join-Path $env:GITHUB_WORKSPACE "cmake-build-ci\tests")
try {
& $env:MSVC_COVERAGE_COLLECTOR collect `
--settings $settingsPath `
--output $coveragePath `
--output-format cobertura `
$testBinary `
--gtest_color=yes `
"--gtest_output=xml:$junitPath"
$collectorExitCode = $LASTEXITCODE
} finally {
Pop-Location
}
if (!$openCppCoverage) {
throw "OpenCppCoverage.exe was not found."
if ($collectorExitCode -ne 0) {
throw "Microsoft coverage collector exited with code $collectorExitCode."
}
if (!(Test-Path -LiteralPath $junitPath)) {
throw "GoogleTest did not produce junit.xml."
}
[xml] $junit = Get-Content -LiteralPath $junitPath -Raw
if ([int] $junit.testsuites.tests -le 0 -or
[int] $junit.testsuites.failures -gt 0 -or [int] $junit.testsuites.errors -gt 0) {
throw "GoogleTest reported failures, errors, or no tests."
}

# The broker test hook compiles a private copy only for failure injection.
& $openCppCoverage `
--sources "$env:GITHUB_WORKSPACE\examples" `
--sources "$env:GITHUB_WORKSPACE\src" `
--sources "$env:GITHUB_WORKSPACE\tools" `
--excluded_sources "$env:GITHUB_WORKSPACE\src\platform\windows\broker" `
"--export_type=cobertura:$env:GITHUB_WORKSPACE\cmake-build-ci\reports\coverage.xml" `
--working_dir "$env:GITHUB_WORKSPACE\cmake-build-ci\tests" `
-- `
"$env:GITHUB_WORKSPACE\cmake-build-ci\tests\$env:CMAKE_BUILD_CONFIG\test_libvirtualhid.exe" `
--gtest_color=yes `
"--gtest_output=xml:$env:GITHUB_WORKSPACE\cmake-build-ci\reports\junit.xml"

- name: Normalize MSVC coverage paths
if: >-
Expand All @@ -466,27 +481,36 @@ jobs:
(steps.test_msvc.outcome == 'success' || steps.test_msvc.outcome == 'failure')
run: |
$coveragePath = Join-Path $env:GITHUB_WORKSPACE "cmake-build-ci\reports\coverage.xml"
if (!(Test-Path $coveragePath)) {
return
if (!(Test-Path -LiteralPath $coveragePath)) {
throw "Microsoft coverage collector did not produce native source coverage."
}

[xml] $coverage = Get-Content $coveragePath
[xml] $coverage = Get-Content -LiteralPath $coveragePath -Raw
$workspace = $env:GITHUB_WORKSPACE.Replace('\', '/')
foreach ($node in $coverage.SelectNodes('//*[@filename]')) {
$filename = $node.GetAttribute('filename').Replace('\', '/')
if ($filename.StartsWith("${workspace}/")) {
if (![IO.Path]::IsPathRooted($filename)) {
foreach ($source in $coverage.SelectNodes('//source')) {
$candidate = $source.InnerText.Replace('\', '/').TrimEnd('/') + '/' + $filename
if ($candidate.StartsWith("${workspace}/", [StringComparison]::OrdinalIgnoreCase)) {
$filename = $candidate
break
}
}
}
if ($filename.StartsWith("${workspace}/", [StringComparison]::OrdinalIgnoreCase)) {
$filename = $filename.Substring($workspace.Length + 1)
}

$node.SetAttribute('filename', $filename)
}

foreach ($source in $coverage.SelectNodes('//source')) {
$source.InnerText = '.'
}

$coverage.Save($coveragePath)

if (!($coverage.SelectSingleNode('/coverage/packages/package/classes/class/lines/line'))) {
throw "Microsoft coverage collector did not produce native source coverage."
}

- name: Generate gcov report
id: test_report
if: >-
Expand Down
11 changes: 10 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ sources and, when enabled, the diagnostic control model. GCC and Clang coverage
instrumentation is confined to the tests directory. The normal library,
examples, tools, and brokers remain free of coverage instrumentation, including
when they are packaged from a build that also runs tests. MSVC coverage uses
OpenCppCoverage rather than compiler instrumentation.
Microsoft.CodeCoverage.Console to instrument only the GoogleTest executable,
which is linked with `/PROFILE`. The collector restores the executable after
the run.

## Windows library and tests

Expand Down Expand Up @@ -37,6 +39,13 @@ table. Run the same check on a local build with MSYS2/UCRT64 binutils installed:
The script checks all built configurations and excludes the separate test
targets. Pass the MSVC build directory to check a Visual Studio build.

MSVC CI configures `tests/msvc-coverage.config.in` for the checkout and invokes
Microsoft.CodeCoverage.Console directly from the
workflow. The collector ships with Visual Studio Enterprise 2022 and all Visual
Studio 2026 editions. Use a Debug test executable with its matching PDB. CI writes
Cobertura `coverage.xml` and GoogleTest `junit.xml`, and fails if tests fail or
native coverage is missing. GCC and Clang builds continue to use gcovr.

## Linux and macOS

```sh
Expand Down
4 changes: 3 additions & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ endif()
set(TEST_BINARY test_libvirtualhid)

# Recompile the library for coverage without instrumenting installed targets.
# MSVC coverage is collected externally with OpenCppCoverage.
# MSVC coverage is collected externally with Microsoft.CodeCoverage.Console.
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
add_compile_options(-fprofile-arcs -ftest-coverage -ggdb -O0)
add_link_options(-fprofile-arcs -ftest-coverage)
Expand Down Expand Up @@ -160,6 +160,8 @@ elseif(WIN32)
LIBVIRTUALHID_TEST_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}")
if(MSVC)
target_compile_options(${TEST_BINARY} PRIVATE /EHsc)
# Only the test executable needs to support native static instrumentation.
target_link_options(${TEST_BINARY} PRIVATE /PROFILE /INCREMENTAL:NO)
endif()
target_link_libraries(${TEST_BINARY}
PRIVATE
Expand Down
26 changes: 26 additions & 0 deletions tests/msvc-coverage.config.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Configuration>
<CodeCoverage>
<EnableStaticNativeInstrumentation>True</EnableStaticNativeInstrumentation>
<EnableStaticNativeInstrumentationRestore>True</EnableStaticNativeInstrumentationRestore>
<EnableDynamicNativeInstrumentation>False</EnableDynamicNativeInstrumentation>
<EnableStaticManagedInstrumentation>False</EnableStaticManagedInstrumentation>
<EnableDynamicManagedInstrumentation>False</EnableDynamicManagedInstrumentation>
<CollectFromChildProcesses>False</CollectFromChildProcesses>
<ModulePaths>
<IncludeDirectories>
<Directory Recursive="false" />
</IncludeDirectories>
<Include>
<ModulePath />
</Include>
</ModulePaths>
<Sources>
<Include>
<Source />
</Include>
<Exclude>
<Source />
</Exclude>
</Sources>
</CodeCoverage>
</Configuration>
Loading