Skip to content
Draft
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
49 changes: 48 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,59 @@ jobs:
name: GVFS_${{ matrix.configuration }}_${{ matrix.architecture }}
path: artifacts\GVFS.Installers

coverage_build:
runs-on: windows-2025
name: Build AOT Coverage Probe
needs: validate
timeout-minutes: 30

env:
AotMsCodeCoverageInstrumentation: 'true'
MsCodeCoverageSessionId: gvfs-ci-${{ github.run_id }}

steps:
- name: Checkout source
if: needs.validate.outputs.skip == ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src
persist-credentials: false

- name: Install .NET SDK
if: needs.validate.outputs.skip == ''
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: src/global.json
dotnet-version: 8.0.x

- name: Add MSBuild to PATH
if: needs.validate.outputs.skip == ''
uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0

- name: Build instrumented x64 installer
if: needs.validate.outputs.skip == ''
shell: cmd
run: src\scripts\Build.bat Release 0.2.173.2 minimal x64

- name: Create build artifacts
if: needs.validate.outputs.skip == ''
shell: cmd
run: src\scripts\CreateBuildArtifacts.bat Release artifacts x64

- name: Upload coverage-probe installer
if: needs.validate.outputs.skip == ''
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: GVFS_Coverage_Release_x64
path: artifacts\GVFS.Installers

functional_tests:
name: Functional Tests
needs: [validate, build]
needs: [validate, build, coverage_build]
uses: ./.github/workflows/functional-tests.yaml
with:
skip: ${{ needs.validate.outputs.skip }}
aot_coverage_probe: true

upgrade_tests:
name: Upgrade Tests
Expand Down
80 changes: 78 additions & 2 deletions .github/workflows/functional-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ on:
required: false
type: string
default: ''
aot_coverage_probe:
description: 'Use the instrumented installer and collect coverage on x64 shard 0'
required: false
type: boolean
default: false
secrets:
vfs_token:
description: 'Token for downloading VFSForGit artifacts (required for cross-repository downloads; defaults to GITHUB_TOKEN)'
Expand All @@ -58,6 +63,8 @@ jobs:
env:
ARTIFACT_PREFIX: ${{ inputs.output_prefix && format('{0}_', inputs.output_prefix) || '' }}
FT_MATRIX_NAME: ${{ format('{0}_{1}-{2}', matrix.configuration, matrix.architecture, matrix.nr) }}
AOT_COVERAGE_PROBE: ${{ inputs.aot_coverage_probe && matrix.architecture == 'x64' && matrix.nr == 0 }}
COVERAGE_SESSION_ID: gvfs-ci-${{ inputs.vfs_run_id || github.run_id }}

strategy:
matrix:
Expand Down Expand Up @@ -104,7 +111,7 @@ jobs:
continue-on-error: true
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: GVFS_${{ matrix.configuration }}_${{ matrix.architecture }}
name: ${{ env.AOT_COVERAGE_PROBE == 'true' && 'GVFS_Coverage_Release_x64' || format('GVFS_{0}_{1}', matrix.configuration, matrix.architecture) }}
path: gvfs
repository: ${{ inputs.vfs_repository || github.repository }}
run-id: ${{ inputs.vfs_run_id || github.run_id }}
Expand All @@ -114,7 +121,7 @@ jobs:
if: steps.skip.outputs.result != 'true' && steps.download-gvfs.outcome == 'failure'
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: GVFS_${{ matrix.configuration }}_${{ matrix.architecture }}
name: ${{ env.AOT_COVERAGE_PROBE == 'true' && 'GVFS_Coverage_Release_x64' || format('GVFS_{0}_{1}', matrix.configuration, matrix.architecture) }}
path: gvfs
repository: ${{ inputs.vfs_repository || github.repository }}
run-id: ${{ inputs.vfs_run_id || github.run_id }}
Expand Down Expand Up @@ -153,6 +160,40 @@ jobs:
run-id: ${{ inputs.vfs_run_id || github.run_id }}
github-token: ${{ secrets.vfs_token || github.token }}

- name: Checkout coverage configuration and source
if: steps.skip.outputs.result != 'true' && env.AOT_COVERAGE_PROBE == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src
persist-credentials: false

- name: Install coverage runtime and SDK
if: steps.skip.outputs.result != 'true' && env.AOT_COVERAGE_PROBE == 'true'
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: src/global.json
dotnet-version: 8.0.x

- name: Install coverage tools
if: steps.skip.outputs.result != 'true' && env.AOT_COVERAGE_PROBE == 'true'
shell: pwsh
run: |
dotnet tool install dotnet-coverage --version 18.8.0 --tool-path coverage-tools --configfile src\nuget.config
if ($LASTEXITCODE -ne 0) { throw 'Coverage collector installation failed.' }
dotnet tool install dotnet-reportgenerator-globaltool --version 5.5.11 --tool-path coverage-tools --configfile src\nuget.config
if ($LASTEXITCODE -ne 0) { throw 'Coverage renderer installation failed.' }
New-Item -ItemType Directory -Path coverage -Force | Out-Null

- name: Start fixed-session coverage before service installation
id: coverage_start
if: steps.skip.outputs.result != 'true' && env.AOT_COVERAGE_PROBE == 'true'
shell: pwsh
run: |
.\coverage-tools\dotnet-coverage.exe collect --server-mode --background --session-id $env:COVERAGE_SESSION_ID --settings src\scripts\AotCoverage.config --output coverage\functional.cobertura.xml --output-format cobertura --log-file coverage\collector.log --log-level Verbose
if ($LASTEXITCODE -ne 0) { throw 'Coverage collector startup failed.' }
.\coverage-tools\dotnet-coverage.exe snapshot $env:COVERAGE_SESSION_ID --output coverage\before-install.coverage
if ($LASTEXITCODE -ne 0) { throw 'Coverage collector readiness check failed.' }

- name: ProjFS details (pre-install)
if: steps.skip.outputs.result != 'true'
shell: cmd
Expand Down Expand Up @@ -197,6 +238,14 @@ jobs:
git\logs
gvfs\logs

- name: Record installed service identity
if: steps.skip.outputs.result != 'true' && env.AOT_COVERAGE_PROBE == 'true'
shell: pwsh
run: |
Get-CimInstance Win32_Service -Filter "Name = 'GVFS.Service'" |
Select-Object Name,StartName,State,ProcessId,PathName |
ConvertTo-Json | Set-Content coverage\installed-service.json

- name: Run functional tests
if: steps.skip.outputs.result != 'true'
shell: cmd
Expand All @@ -207,6 +256,33 @@ jobs:
SET GVFS_TEST_DIAGNOSTICS_DIR=C:\temp\gvfs-ft-diagnostics
ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci --slice=${{ matrix.nr }},12

- name: Stop coverage and verify SCM service capture
id: coverage_stop
if: always() && env.AOT_COVERAGE_PROBE == 'true' && steps.coverage_start.outcome == 'success'
shell: pwsh
run: |
.\coverage-tools\dotnet-coverage.exe shutdown $env:COVERAGE_SESSION_ID
if ($LASTEXITCODE -ne 0) { throw 'Coverage collector shutdown failed.' }
src\scripts\Assert-AotCoverage.ps1 -CoveragePath coverage\functional.cobertura.xml |
Tee-Object -FilePath coverage\probe.json

- name: Render single-shard coverage report
if: always() && env.AOT_COVERAGE_PROBE == 'true' && steps.coverage_stop.outcome == 'success'
shell: pwsh
run: |
.\coverage-tools\reportgenerator.exe -reports:coverage\functional.cobertura.xml -targetdir:coverage\report "-reporttypes:Html;MarkdownSummaryGithub;Cobertura;JsonSummary" "-filefilters:+$env:GITHUB_WORKSPACE\src\GVFS\*" "-title:Managed NativeAOT product - x64 shard 0 of 12"
if ($LASTEXITCODE -ne 0) { throw 'Coverage rendering failed.' }
'One-shard probe only. Native C++ and the other shards are not measured.' >> $env:GITHUB_STEP_SUMMARY
Get-Content coverage\report\SummaryGithub.md >> $env:GITHUB_STEP_SUMMARY

- name: Upload coverage-probe evidence
if: always() && steps.skip.outputs.result != 'true' && env.AOT_COVERAGE_PROBE == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ env.ARTIFACT_PREFIX }}AotCoverage_${{ env.FT_MATRIX_NAME }}
path: coverage
if-no-files-found: error

- name: Upload failure diagnostics (mount dumps + logs)
if: always() && steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand Down
9 changes: 9 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
<!-- Include custom MSBuild targets/tasks -->
<Import Project="$(MSBuildThisFileDirectory)GVFS\GVFS.MSBuild\GVFS.targets" />

<!-- Coverage builds use separate, non-shipping payloads. Normal publishes omit this package. -->
<PropertyGroup Condition="'$(AotMsCodeCoverageInstrumentation)' == 'true' and '$(MSBuildProjectExtension)' == '.csproj'">
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>

<ItemGroup Condition="'$(AotMsCodeCoverageInstrumentation)' == 'true' and '$(PublishAot)' == 'true' and '$(OutputType)' == 'Exe'">
<PackageReference Include="Microsoft.CodeCoverage.MSBuild" PrivateAssets="all" />
</ItemGroup>

<!--
Auto-restore vcpkg native dependencies when missing.
Modeled after vcpkg's official VcpkgInstallManifestDependencies target
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<PackageVersion Include="System.CommandLine" Version="2.0.5" />

<!-- Testing -->
<PackageVersion Include="Microsoft.CodeCoverage.MSBuild" Version="18.8.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Moq" Version="4.10.1" />
<PackageVersion Include="NUnit3TestAdapter" Version="3.13.0" />
Expand Down
11 changes: 11 additions & 0 deletions scripts/AotCoverage.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<IncludeTestAssembly>False</IncludeTestAssembly>
<CodeCoverage>
<EnableDynamicManagedInstrumentation>False</EnableDynamicManagedInstrumentation>
<EnableStaticManagedInstrumentation>False</EnableStaticManagedInstrumentation>
<EnableDynamicNativeInstrumentation>False</EnableDynamicNativeInstrumentation>
<EnableStaticNativeInstrumentation>False</EnableStaticNativeInstrumentation>
<CollectFromChildProcesses>True</CollectFromChildProcesses>
</CodeCoverage>
</Configuration>
78 changes: 78 additions & 0 deletions scripts/Assert-AotCoverage.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
BeforeAll {
$assertion = Join-Path $PSScriptRoot 'Assert-AotCoverage.ps1'

function New-CoverageReport {
param(
[int]$OnStartHits = 1,
[int]$ConsoleHits = 0
)

$modules = @('GVFS', 'GVFS.Hooks', 'GVFS.Mount', 'GVFS.Service', 'GVFS.Common', 'GVFS.Platform.Windows', 'GVFS.Virtualization')
$packages = ($modules | ForEach-Object {
"<package name='$_'><classes><class name='$_.Type'><lines><line number='1' hits='1' /></lines></class></classes></package>"
}) -join ''
[xml]$document = "<coverage lines-covered='8' lines-valid='9'><packages>$packages</packages></coverage>"
$service = $document.SelectSingleNode("//package[@name='GVFS.Service']/classes")
$service.InnerXml = @"
<class name="GVFS.Service.GVFSService">
<methods><method name="OnStart"><lines><line number="1" hits="$OnStartHits" /></lines></method></methods>
<lines><line number="1" hits="$OnStartHits" /></lines>
</class>
<class name="GVFS.Service.Program">
<methods><method name="RunAsConsoleApp"><lines><line number="42" hits="$ConsoleHits" /></lines></method></methods>
<lines><line number="20" hits="1" /><line number="42" hits="$ConsoleHits" /></lines>
</class>
"@
return ,$document
}

function Save-CoverageReport {
param([xml]$Document)
$path = Join-Path $TestDrive 'coverage.xml'
$Document.Save($path)
return $path
}
}

Describe 'NativeAOT SCM coverage evidence' {
It 'Accepts product coverage with SCM startup and no console-service entry' {
$path = Save-CoverageReport (New-CoverageReport)
$result = & $assertion -CoveragePath $path | ConvertFrom-Json
$result.ScmServiceStartupCovered | Should -BeTrue
$result.CompleteSuiteCoverage | Should -BeFalse
}

It 'Rejects a report without SCM startup execution' {
$path = Save-CoverageReport (New-CoverageReport -OnStartHits 0)
{ & $assertion -CoveragePath $path } | Should -Throw '*no executed GVFSService.OnStart*'
}

It 'Rejects console-service execution even when SCM startup also executes' {
$path = Save-CoverageReport (New-CoverageReport -ConsoleHits 1)
{ & $assertion -CoveragePath $path } | Should -Throw '*Console-service execution contaminates*'
}

It 'Rejects missing product modules' {
$document = New-CoverageReport
$node = $document.SelectSingleNode("//package[@name='GVFS.Mount']")
[void]$node.ParentNode.RemoveChild($node)
$path = Save-CoverageReport $document
{ & $assertion -CoveragePath $path } | Should -Throw '*Expected one coverage module for GVFS.Mount*'
}

It 'Rejects product modules with metadata but no executed lines' {
$document = New-CoverageReport
$document.SelectSingleNode("//package[@name='GVFS.Hooks']//line").SetAttribute('hits', '0')
$path = Save-CoverageReport $document
{ & $assertion -CoveragePath $path } | Should -Throw '*No executed lines in required product module GVFS.Hooks*'
}

It 'Rejects unit-copy coverage in a product report' {
$document = New-CoverageReport
$node = $document.CreateElement('package')
$node.SetAttribute('name', 'GVFS.UnitTests')
[void]$document.coverage.packages.AppendChild($node)
$path = Save-CoverageReport $document
{ & $assertion -CoveragePath $path } | Should -Throw '*Test assemblies contaminate*'
}
}
61 changes: 61 additions & 0 deletions scripts/Assert-AotCoverage.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
param(
[Parameter(Mandatory)]
[string]$CoveragePath
)

$ErrorActionPreference = 'Stop'

[xml]$coverage = Get-Content -LiteralPath $CoveragePath -Raw
$packages = @($coverage.coverage.packages.package)
$requiredModules = @(
'GVFS',
'GVFS.Hooks',
'GVFS.Mount',
'GVFS.Service',
'GVFS.Common',
'GVFS.Platform.Windows',
'GVFS.Virtualization'
)

foreach ($name in $requiredModules) {
$module = @($packages | Where-Object { $_.name -eq $name })
if ($module.Count -ne 1) {
throw "Expected one coverage module for $name; found $($module.Count)."
}

$hitLines = @($module[0].classes.class.lines.line | Where-Object { [int]$_.hits -gt 0 })
if ($hitLines.Count -eq 0) {
throw "No executed lines in required product module $name."
}
}

$testModules = @($packages | Where-Object { $_.name -match '(UnitTests|FunctionalTests|^GVFS.Tests$)' })
if ($testModules.Count -ne 0) {
throw "Test assemblies contaminate the product report: $($testModules.name -join ', ')."
}

$service = $packages | Where-Object { $_.name -eq 'GVFS.Service' }
$serviceClass = $service.classes.class | Where-Object { $_.name -eq 'GVFS.Service.GVFSService' }
$onStart = @($serviceClass.methods.method | Where-Object { $_.name -eq 'OnStart' })
$startupHits = @($onStart.lines.line | Where-Object { [int]$_.hits -gt 0 })
if ($startupHits.Count -eq 0) {
throw 'The report contains no executed GVFSService.OnStart lines.'
}

$program = $service.classes.class | Where-Object { $_.name -eq 'GVFS.Service.Program' }
$consoleEntry = @($program.methods.method | Where-Object { $_.name -eq 'RunAsConsoleApp' })
$consoleHits = @($consoleEntry.lines.line | Where-Object { [int]$_.hits -gt 0 })
if ($consoleHits.Count -ne 0) {
throw 'Console-service execution contaminates the SCM-service probe.'
}

[ordered]@{
Scope = 'Managed NativeAOT product; x64 shard 0 of 12'
Modules = $packages.name
ScmServiceStartupCovered = $true
ConsoleServiceEntryCovered = $false
NativeCppCovered = $false
CompleteSuiteCoverage = $false
CoveredLines = [int]$coverage.coverage.'lines-covered'
MeasuredLines = [int]$coverage.coverage.'lines-valid'
} | ConvertTo-Json -Depth 3
Loading