diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 47b1a32..ac2e7cd 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -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: @@ -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' @@ -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: >- @@ -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: >- diff --git a/docs/development.md b/docs/development.md index 38df3e9..5717428 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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 @@ -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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 78ff24e..1331a71 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) @@ -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 diff --git a/tests/msvc-coverage.config.in b/tests/msvc-coverage.config.in new file mode 100644 index 0000000..6a7d244 --- /dev/null +++ b/tests/msvc-coverage.config.in @@ -0,0 +1,26 @@ + + + True + True + False + False + False + False + + + + + + + + + + + + + + + + + +