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
12 changes: 12 additions & 0 deletions .github/actions/setup-coverage/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Set up coverage collector
description: Locate the Visual Studio collector with cross-account service support.
runs:
using: composite
steps:
- shell: pwsh
run: |
$collectors = @(& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -products * -latest -find Common7\IDE\Extensions\Microsoft\CodeCoverage.Console\Microsoft.CodeCoverage.Console.exe)
if ($LASTEXITCODE -ne 0 -or $collectors.Count -ne 1) { throw 'Expected one Visual Studio coverage collector.' }
Split-Path -Parent $collectors[0] >> $env:GITHUB_PATH
& $collectors[0] --version
if ($LASTEXITCODE -ne 0) { throw 'Visual Studio coverage collector cannot start.' }
109 changes: 107 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ jobs:
runs-on: ${{ matrix.architecture == 'arm64' && 'windows-11-arm' || 'windows-2025' }}
name: Build and Unit Test
needs: validate
env:
AotMsCodeCoverageInstrumentation: 'true'
MsCodeCoverageSessionId: gvfs-ci-${{ github.run_id }}

strategy:
matrix:
Expand All @@ -280,12 +283,14 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src
persist-credentials: false

- name: Install .NET SDK
if: steps.skip.outputs.result != 'true'
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: steps.skip.outputs.result != 'true'
Expand All @@ -296,16 +301,54 @@ jobs:
shell: cmd
run: src\scripts\Build.bat ${{ matrix.configuration }} 0.2.173.2 minimal ${{ matrix.architecture }}

- name: Locate coverage collector
if: steps.skip.outputs.result != 'true'
uses: ./src/.github/actions/setup-coverage

- name: Record compiled coverage manifest
if: steps.skip.outputs.result != 'true'
shell: pwsh
env:
COVERAGE_ARCH: ${{ matrix.architecture }}
run: src\scripts\New-CoverageManifest.ps1 -Path coverage-build.json -Repository $env:GITHUB_REPOSITORY -SessionId $env:MsCodeCoverageSessionId -Architecture $env:COVERAGE_ARCH

- name: Start unit coverage
id: unit_coverage
if: steps.skip.outputs.result != 'true'
shell: pwsh
run: src\scripts\Invoke-Coverage.ps1 -Action Start -Directory unit-coverage -BuildManifest coverage-build.json -Suite Unit

- name: Run unit tests
id: unit_tests
if: steps.skip.outputs.result != 'true'
shell: cmd
run: src\scripts\RunUnitTests.bat ${{ matrix.configuration }} ${{ matrix.architecture }}
shell: pwsh
run: |
Microsoft.CodeCoverage.Console.exe connect $env:MsCodeCoverageSessionId cmd.exe /d /c src\scripts\RunUnitTests.bat ${{ matrix.configuration }} ${{ matrix.architecture }}
exit $LASTEXITCODE

- name: Stop unit coverage
if: always() && steps.unit_coverage.outcome == 'success'
shell: pwsh
run: src\scripts\Invoke-Coverage.ps1 -Action Stop -Directory unit-coverage -TestOutcome '${{ steps.unit_tests.outcome }}'

- name: Upload unit coverage
if: always() && steps.unit_coverage.outcome == 'success'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: Coverage_Unit_${{ matrix.architecture }}
path: unit-coverage
if-no-files-found: error

- name: Create build artifacts
if: steps.skip.outputs.result != 'true'
shell: cmd
run: src\scripts\CreateBuildArtifacts.bat ${{ matrix.configuration }} artifacts ${{ matrix.architecture }}

- name: Include coverage manifest with installer
if: steps.skip.outputs.result != 'true'
shell: pwsh
run: Copy-Item coverage-build.json artifacts\GVFS.Installers\coverage-build.json

- name: Upload functional tests drop
if: steps.skip.outputs.result != 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand Down Expand Up @@ -341,6 +384,68 @@ jobs:
with:
skip: ${{ needs.validate.outputs.skip }}

coverage_summary:
name: Managed Code Coverage
runs-on: windows-2025
needs: [validate, build, functional_tests, upgrade_tests]
if: always() && needs.validate.outputs.skip == ''
steps:
- name: Checkout tested source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src
fetch-depth: 0
persist-credentials: false

- name: Install report runtime
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: src/global.json
dotnet-version: 8.0.x

- name: Download all coverage workers
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: Coverage_*
path: coverage-inputs

- name: Verify workers and changed-line evidence
shell: pwsh
env:
BASE_COMMIT: ${{ github.event.pull_request.base.sha || github.event.before }}
IS_PULL_REQUEST: ${{ github.event_name == 'pull_request' }}
run: |
$sourceCommit = git -C src rev-parse HEAD
if ($LASTEXITCODE -ne 0) { throw 'Cannot identify tested source.' }
$base = $env:BASE_COMMIT
if ($env:IS_PULL_REQUEST -eq 'true') {
# The event base can lag the target used by the tested merge.
$base = git -C src rev-parse "$sourceCommit^1"
if ($LASTEXITCODE -ne 0) { throw 'Cannot identify the tested merge base.' }
}
if ($base -match '^0+$') { $base = '' }
src\scripts\Merge-Coverage.ps1 -InputDirectory coverage-inputs -OutputDirectory coverage-report -SourceDirectory src -SourceCommit $sourceCommit -Repository $env:GITHUB_REPOSITORY -BaseCommit $base
Get-Content coverage-report\Summary.md >> $env:GITHUB_STEP_SUMMARY
Get-Content coverage-report\ChangedLines.md >> $env:GITHUB_STEP_SUMMARY

- name: Render full managed reports
shell: pwsh
run: |
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.' }
foreach ($group in Get-ChildItem coverage-report\normalized -Directory) {
.\coverage-tools\reportgenerator.exe "-reports:$($group.FullName)\*.cobertura.xml" "-targetdir:coverage-report\$($group.Name)" "-reporttypes:Html;MarkdownSummaryGithub;Cobertura;JsonSummary" "-title:Managed source coverage - $($group.Name)"
if ($LASTEXITCODE -ne 0) { throw "Coverage rendering failed for $($group.Name)." }
}

- name: Upload complete coverage reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ManagedCoverage
path: coverage-report
if-no-files-found: error

result:
runs-on: ubuntu-latest
name: Build, Unit and Functional Tests Successful
Expand Down
75 changes: 75 additions & 0 deletions .github/workflows/functional-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,46 @@ jobs:
run-id: ${{ inputs.vfs_run_id || github.run_id }}
github-token: ${{ secrets.vfs_token || github.token }}

- name: Read installer coverage manifest
id: coverage_manifest
if: steps.skip.outputs.result != 'true'
shell: pwsh
env:
EXPECTED_ARCH: ${{ matrix.architecture }}
run: |
if (Test-Path gvfs\coverage-build.json) {
$manifest = Get-Content gvfs\coverage-build.json -Raw | ConvertFrom-Json
if ($manifest.SchemaVersion -ne 1 -or $manifest.SourceCommit -notmatch '^[a-f0-9]{40}$' -or $manifest.Repository -notmatch '^[\w.-]+/[\w.-]+$' -or $manifest.Architecture -ne $env:EXPECTED_ARCH -or $manifest.SessionId -notmatch '^[\w.-]+$') {
throw 'Invalid installer coverage manifest.'
}
"enabled=true" >> $env:GITHUB_OUTPUT
"source_commit=$($manifest.SourceCommit)" >> $env:GITHUB_OUTPUT
"repository=$($manifest.Repository)" >> $env:GITHUB_OUTPUT
"session_id=$($manifest.SessionId)" >> $env:GITHUB_OUTPUT
} else {
"enabled=false" >> $env:GITHUB_OUTPUT
}

- name: Checkout compiled coverage source
if: steps.skip.outputs.result != 'true' && steps.coverage_manifest.outputs.enabled == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src
repository: ${{ steps.coverage_manifest.outputs.repository }}
ref: ${{ steps.coverage_manifest.outputs.source_commit }}
token: ${{ secrets.vfs_token || github.token }}
persist-credentials: false

- name: Locate coverage collector
if: steps.skip.outputs.result != 'true' && steps.coverage_manifest.outputs.enabled == 'true'
uses: ./src/.github/actions/setup-coverage

- name: Start fixed-session coverage before service installation
id: coverage_start
if: steps.skip.outputs.result != 'true' && steps.coverage_manifest.outputs.enabled == 'true'
shell: pwsh
run: src\scripts\Invoke-Coverage.ps1 -Action Start -Directory coverage -BuildManifest gvfs\coverage-build.json -Suite Functional -Shard ${{ matrix.nr }}

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

- name: Record installed service identity
if: steps.skip.outputs.result != 'true' && steps.coverage_manifest.outputs.enabled == '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: Verify service coverage before functional tests
if: steps.skip.outputs.result != 'true' && steps.coverage_manifest.outputs.enabled == 'true'
shell: pwsh
env:
COVERAGE_SESSION_ID: ${{ steps.coverage_manifest.outputs.session_id }}
run: |
Microsoft.CodeCoverage.Console.exe snapshot $env:COVERAGE_SESSION_ID --output coverage\after-install.coverage
if ($LASTEXITCODE -ne 0) { throw 'Post-installation coverage snapshot failed.' }
Microsoft.CodeCoverage.Console.exe merge coverage\after-install.coverage --output coverage\after-install.cobertura.xml --output-format cobertura
if ($LASTEXITCODE -ne 0) { throw 'Post-installation coverage conversion failed.' }
src\scripts\Assert-AotCoverage.ps1 -CoveragePath coverage\after-install.cobertura.xml -ServiceOnly |
Tee-Object -FilePath coverage\service-startup-probe.json

- name: Run functional tests
id: functional_tests
if: steps.skip.outputs.result != 'true'
shell: cmd
timeout-minutes: 60
Expand All @@ -207,6 +269,19 @@ 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
if: always() && steps.coverage_start.outcome == 'success'
shell: pwsh
run: src\scripts\Invoke-Coverage.ps1 -Action Stop -Directory coverage -TestOutcome '${{ steps.functional_tests.outcome }}'

- name: Upload worker coverage
if: always() && steps.coverage_start.outcome == 'success'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ env.ARTIFACT_PREFIX }}Coverage_Functional_${{ matrix.architecture }}_${{ matrix.nr }}
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
50 changes: 50 additions & 0 deletions .github/workflows/upgrade-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,42 @@ jobs:
name: GVFS_${{ matrix.configuration }}_x64
path: gvfs-new

- name: Read installer coverage manifest
id: coverage_manifest
if: steps.skip.outputs.result != 'true'
shell: pwsh
run: |
if (Test-Path gvfs-new\coverage-build.json) {
$manifest = Get-Content gvfs-new\coverage-build.json -Raw | ConvertFrom-Json
if ($manifest.SchemaVersion -ne 1 -or $manifest.SourceCommit -notmatch '^[a-f0-9]{40}$' -or $manifest.Repository -notmatch '^[\w.-]+/[\w.-]+$' -or $manifest.Architecture -ne 'x64' -or $manifest.SessionId -notmatch '^[\w.-]+$') {
throw 'Invalid installer coverage manifest.'
}
"enabled=true" >> $env:GITHUB_OUTPUT
"source_commit=$($manifest.SourceCommit)" >> $env:GITHUB_OUTPUT
"repository=$($manifest.Repository)" >> $env:GITHUB_OUTPUT
} else {
"enabled=false" >> $env:GITHUB_OUTPUT
}

- name: Checkout compiled coverage source
if: steps.skip.outputs.result != 'true' && steps.coverage_manifest.outputs.enabled == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src
repository: ${{ steps.coverage_manifest.outputs.repository }}
ref: ${{ steps.coverage_manifest.outputs.source_commit }}
persist-credentials: false

- name: Locate coverage collector
if: steps.skip.outputs.result != 'true' && steps.coverage_manifest.outputs.enabled == 'true'
uses: ./src/.github/actions/setup-coverage

- name: Start coverage before upgrade scenario
id: coverage_start
if: steps.skip.outputs.result != 'true' && steps.coverage_manifest.outputs.enabled == 'true'
shell: pwsh
run: src\scripts\Invoke-Coverage.ps1 -Action Start -Directory coverage -BuildManifest gvfs-new\coverage-build.json -Suite Upgrade -Scenario '${{ matrix.scenario }}'

# -- Setup --

- name: Install Git
Expand All @@ -113,6 +149,7 @@ jobs:
# -- Test Execution --

- name: Run upgrade test - ${{ matrix.scenario }}
id: upgrade_test
if: steps.skip.outputs.result != 'true'
shell: pwsh
run: |
Expand Down Expand Up @@ -385,6 +422,19 @@ jobs:
}
}

- name: Stop upgrade coverage
if: always() && steps.coverage_start.outcome == 'success'
shell: pwsh
run: src\scripts\Invoke-Coverage.ps1 -Action Stop -Directory coverage -TestOutcome '${{ steps.upgrade_test.outcome }}'

- name: Upload upgrade coverage
if: always() && steps.coverage_start.outcome == 'success'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: Coverage_Upgrade_x64_${{ matrix.scenario }}
path: coverage
if-no-files-found: error

- name: Upload service 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" />

<!-- CI opts into coverage. Release and local publishes omit this package by default. -->
<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
14 changes: 14 additions & 0 deletions scripts/AotCoverage.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration>
<IncludeTestAssembly>False</IncludeTestAssembly>
<CodeCoverage>
<AllowedUsers>
<User>NT AUTHORITY\SYSTEM</User>
</AllowedUsers>
<EnableDynamicManagedInstrumentation>False</EnableDynamicManagedInstrumentation>
<EnableStaticManagedInstrumentation>False</EnableStaticManagedInstrumentation>
<EnableDynamicNativeInstrumentation>False</EnableDynamicNativeInstrumentation>
<EnableStaticNativeInstrumentation>False</EnableStaticNativeInstrumentation>
<CollectFromChildProcesses>True</CollectFromChildProcesses>
</CodeCoverage>
</Configuration>
Loading
Loading