mirror of
https://github.com/microsoft/mcp.git
synced 2026-11-04 12:01:31 +00:00
* Check HasLiveTests and HasUnitTests against '-eq True' * Fix spelling * Switch to clearer [bool]::Parse * Use TryParse for saefty * Fix TryParse usage * Doc cleanup
336 lines
12 KiB
PowerShell
Executable File
336 lines
12 KiB
PowerShell
Executable File
#!/usr/bin/env pwsh
|
|
#Requires -Version 7
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string[]] $Paths,
|
|
[ValidateSet('Live', 'Unit', 'All', 'Recorded')]
|
|
[string] $TestType = 'Unit',
|
|
[string] $TestResultsPath,
|
|
[switch] $CollectCoverage,
|
|
[switch] $OpenReport,
|
|
[switch] $TestNativeBuild,
|
|
[switch] $OnlyBuild,
|
|
[string] $ScopingBuildInfoPath = $null
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
. "$PSScriptRoot/../common/scripts/common.ps1"
|
|
|
|
$RepoRoot = $RepoRoot.Path.Replace('\', '/')
|
|
|
|
$debugLogs = $env:SYSTEM_DEBUG -eq 'true' -or $DebugPreference -eq 'Continue'
|
|
|
|
$BuildInfo = $null
|
|
if ($ScopingBuildInfoPath) {
|
|
if (!(Test-Path $ScopingBuildInfoPath)) {
|
|
Write-Error "BuildInfo path was provided, but not found at path: $ScopingBuildInfoPath"
|
|
}
|
|
$BuildInfo = Get-Content $ScopingBuildInfoPath -Raw | ConvertFrom-Json -AsHashtable
|
|
}
|
|
|
|
$workPath = "$RepoRoot/.work/tests"
|
|
Remove-Item -Recurse -Force $workPath -ErrorAction SilentlyContinue
|
|
New-Item -ItemType Directory -Path $workPath -Force | Out-Null
|
|
|
|
if (!$TestResultsPath) {
|
|
$TestResultsPath = "$workPath/testResults"
|
|
}
|
|
|
|
# Clean previous results
|
|
Remove-Item -Recurse -Force $TestResultsPath -ErrorAction SilentlyContinue
|
|
|
|
# Finds all test projects, then filters them based on the specified path filters.
|
|
function FilterTestProjects {
|
|
$testProjects = Get-ChildItem -Path "$RepoRoot" -Recurse -Filter "*Tests.csproj" -File
|
|
| Where-Object {
|
|
$testProjectDetails = & "$($PSScriptRoot)/Get-ProjectProperties.ps1" -Path $_.FullName
|
|
# Need to parse $testProjectDetails.HasLiveTests and HasUnitTests as they're based on JSON values, therefore will not be a PowerShell boolean
|
|
$result = $false
|
|
return ([bool]::TryParse($testProjectDetails.HasLiveTests, [ref]$result) -and $result -and $TestType -in @('Live', 'Recorded', 'All')) -or
|
|
([bool]::TryParse($testProjectDetails.HasUnitTests, [ref]$result) -and $result -and $TestType -in @('Unit', 'All'))
|
|
}
|
|
| ForEach-Object { @{
|
|
FullName = $_.FullName
|
|
Relative = (Resolve-Path -Path $_.FullName -Relative -RelativeBasePath $RepoRoot).Replace('\', '/').TrimStart('./')
|
|
}}
|
|
|
|
# if provided a buildinfo, further scope the tests to only those impacted by changes
|
|
if ($BuildInfo){
|
|
$changedPaths = $BuildInfo.pathsToTest | ForEach-Object { $_.path }
|
|
|
|
$testProjects = $testProjects | Where-Object {
|
|
$testProjectPath = $_.Relative
|
|
($changedPaths | Where-Object { $testProjectPath.StartsWith($_) }).Count -gt 0
|
|
}
|
|
}
|
|
|
|
if ($testType -eq 'Recorded') {
|
|
# until all LiveTest projects are migrated to recorded tests, we _must_ complete
|
|
# an additional filter such that we'll only invoke those csprojs where playback is possible
|
|
$testProjects = $testProjects | Where-Object {
|
|
$projectDirectory = Split-Path -Path $_.FullName -Parent
|
|
Test-Path -Path (Join-Path -Path $projectDirectory -ChildPath 'assets.json')
|
|
}
|
|
}
|
|
|
|
$normalizedPathFilters = $Paths ? ($Paths | ForEach-Object { "*$($_.Replace('\', '/'))*" }) : @()
|
|
|
|
if($normalizedPathFilters) {
|
|
$testProjects = $testProjects | Where-Object {
|
|
foreach($filter in $normalizedPathFilters) {
|
|
if ($_.Relative -like $filter) {
|
|
return $true
|
|
}
|
|
}
|
|
return $false
|
|
}
|
|
}
|
|
|
|
if($testProjects.Count -eq 0) {
|
|
Write-Error "No test projects found for test type '$testType' with the specified filters"
|
|
return $null
|
|
}
|
|
|
|
return $testProjects.FullName
|
|
}
|
|
|
|
function CreateTestSolution {
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$workPath,
|
|
[Parameter(Mandatory=$true)]
|
|
[string[]]$testProjects
|
|
)
|
|
|
|
# Create solution and add projects
|
|
Write-Host "Creating temporary solution file..."
|
|
|
|
Push-Location $workPath
|
|
try {
|
|
dotnet new sln -n "Tests" | Out-Null
|
|
dotnet sln add $testProjects --in-root --include-references false | Out-Host
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
|
|
# .NET 10+ creates .slnx by default instead of .sln
|
|
$slnFile = Get-ChildItem -Path $workPath -Filter "Tests.sln*" -File | Select-Object -First 1
|
|
if (-not $slnFile) {
|
|
Write-Error "Failed to create temporary solution file in $workPath"
|
|
return $null
|
|
}
|
|
|
|
return $slnFile.FullName.Replace('\', '/')
|
|
}
|
|
|
|
function Create-CoverageReport {
|
|
# Find the coverage files
|
|
$coverageFiles = Get-ChildItem -Path $TestResultsPath -Recurse -Filter "coverage.cobertura.*.xml"
|
|
| Where-Object { $_.FullName.Replace('\','/') -notlike "*/in/*" }
|
|
|
|
if (-not $coverageFiles -or $coverageFiles.Count -eq 0) {
|
|
Write-Error "No coverage file found!"
|
|
exit 1
|
|
}
|
|
|
|
if (-not (Get-Command dotnet-coverage -ErrorAction SilentlyContinue)) {
|
|
Write-Host "Installing dotnet-coverage tool..."
|
|
dotnet tool install -g dotnet-coverage
|
|
}
|
|
|
|
$mergedFile = "$TestResultsPath/coverage.merged.cobertura.xml"
|
|
Write-Host "Merging coverage files into $mergedFile..."
|
|
Invoke-LoggedCommand ("dotnet-coverage merge $TestResultsPath/coverage.cobertura.*.xml" +
|
|
" --output '$mergedFile'" +
|
|
" --output-format cobertura")
|
|
|
|
if ($env:TF_BUILD) {
|
|
# Write the path to the cover file to a pipeline variable
|
|
Write-Host "##vso[task.setvariable variable=CoverageFile]$($mergedFile)"
|
|
} else {
|
|
# Ensure reportgenerator tool is installed
|
|
if (-not (Get-Command reportgenerator -ErrorAction SilentlyContinue)) {
|
|
Write-Host "Installing reportgenerator tool..."
|
|
dotnet tool install -g dotnet-reportgenerator-globaltool
|
|
}
|
|
|
|
# Generate reports
|
|
Write-Host "Generating coverage reports..."
|
|
|
|
$reportDirectory = "$TestResultsPath/coverageReport"
|
|
Invoke-LoggedCommand ("reportgenerator" +
|
|
" -reports:'$mergedFile'" +
|
|
" -targetdir:'$reportDirectory'" +
|
|
" -reporttypes:'Html;HtmlSummary;Cobertura'" +
|
|
" -assemblyfilters:'+azmcp'" +
|
|
" -classfilters:'-*Tests*;-*Program'" +
|
|
" -filefilters:'-*JsonSourceGenerator*;-*LibraryImportGenerator*'")
|
|
|
|
Write-Host "Coverage report generated at $reportDirectory/index.html"
|
|
|
|
# Open the report in default browser
|
|
$reportPath = "$reportDirectory/index.html"
|
|
if (-not (Test-Path $reportPath)) {
|
|
Write-Error "Could not find coverage report at $reportPath"
|
|
exit 1
|
|
}
|
|
|
|
if ($OpenReport) {
|
|
# Open the report in default browser
|
|
Write-Host "Opening coverage report in browser..."
|
|
if ($IsMacOS) {
|
|
# On macOS, use 'open' command
|
|
Start-Process "open" -ArgumentList $reportPath
|
|
} elseif ($IsLinux) {
|
|
# On Linux, use 'xdg-open'
|
|
Start-Process "xdg-open" -ArgumentList $reportPath
|
|
} else {
|
|
# On Windows, use 'Start-Process'
|
|
Start-Process $reportPath
|
|
}
|
|
}
|
|
}
|
|
|
|
# Command Coverage Summary
|
|
try{
|
|
$CommandCoverageSummaryFile = "$TestResultsPath/Coverage.md"
|
|
|
|
$xml = [xml](Get-Content $mergedFile)
|
|
|
|
$classes = $xml.coverage.packages.package.classes.class |
|
|
Where-Object { $_.name -match 'AzureMcp\.(.*\.)?Commands\.' -and $_.filename -notlike '*System.Text.Json.SourceGeneration*' }
|
|
|
|
$fileGroups = $classes |
|
|
Group-Object { $_.filename } |
|
|
Sort-Object Name
|
|
|
|
$summary = $fileGroups | ForEach-Object {
|
|
# for live tests, we only want to look at the ExecuteAsync methods
|
|
$methods = if($Live) {
|
|
$_.Group | ForEach-Object {
|
|
if($_.name -like '*<ExecuteAsync>*'){
|
|
# Generated code for async ExecuteAsync methods
|
|
return $_.methods.method
|
|
} else {
|
|
# Non async methods named ExecuteAsync
|
|
return $_.methods.method | Where-Object { $_.name -eq 'ExecuteAsync' }
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$_.Group.methods.method
|
|
}
|
|
|
|
$lines = $methods.lines.line
|
|
$covered = ($lines | Where-Object { $_.hits -gt 0 }).Count
|
|
$total = $lines.Count
|
|
|
|
if($total) {
|
|
return [pscustomobject]@{
|
|
file = $_.name
|
|
pct = if ($total -gt 0) { $covered * 100 / $total } else { 0 }
|
|
covered = $covered
|
|
lines = $total
|
|
}
|
|
}
|
|
}
|
|
|
|
$maxFileWidth = ($summary | Measure-Object { $_.file.Length } -Maximum).Maximum
|
|
if ($maxFileWidth -le 0) {
|
|
$maxFileWidth = 10
|
|
}
|
|
$header = $live ? "Live test code coverage for command ExecuteAsync methods" : "Unit test code coverage for command classes"
|
|
|
|
$output = ($env:TF_BUILD ? "" : "$header`n`n") +
|
|
"File $(' ' * ($maxFileWidth - 5)) | % Covered | Lines | Covered`n" +
|
|
"$('-' * $maxFileWidth) | --------: | ----: | ------:`n"
|
|
|
|
$summary | ForEach-Object {
|
|
# Format each line with the appropriate width
|
|
$output += ("{0,-$maxFileWidth} | {1,9:F0} | {2,5} | {3,7}`n" -f $_.file, $_.pct, $_.lines, $_.covered)
|
|
}
|
|
|
|
Write-Host "Writing command coverage summary to $CommandCoverageSummaryFile"
|
|
$output | Out-File -FilePath $CommandCoverageSummaryFile -Encoding utf8
|
|
|
|
if ($env:TF_BUILD) {
|
|
Write-Host "##vso[task.addattachment type=Distributedtask.Core.Summary;name=$header;]$(Resolve-Path $CommandCoverageSummaryFile)"
|
|
}
|
|
}
|
|
catch {
|
|
Write-Host "Error creating coverage summary: $($_.Exception.Message)"
|
|
Write-Host "Stack trace: $($_.Exception.StackTrace)"
|
|
exit 1
|
|
}
|
|
}
|
|
# main
|
|
|
|
$testProjects = FilterTestProjects
|
|
|
|
$solutionPath = CreateTestSolution -workPath $workPath -testProjects $testProjects
|
|
|
|
if (!$solutionPath) {
|
|
exit 1
|
|
}
|
|
|
|
Push-Location $workPath
|
|
try {
|
|
if($debugLogs) {
|
|
Write-Host "`n`n"
|
|
# dump all environment variables
|
|
Write-Host "Current environment variables:" -ForegroundColor Yellow
|
|
Get-ChildItem Env: | Sort-Object Name | ForEach-Object { "$($_.Name)= $($_.Value)" } | Out-Host
|
|
|
|
# dump az powershell context
|
|
Write-Host "`nCurrent Azure PowerShell context (Get-AzContext):" -ForegroundColor Yellow
|
|
try {
|
|
Get-AzContext | ConvertTo-Json | Out-Host
|
|
} catch {
|
|
Write-Host "Error retrieving Azure PowerShell context: $($_.Exception.Message)" -ForegroundColor Red
|
|
}
|
|
|
|
# dump az cli context
|
|
Write-Host "`nCurrent Azure CLI context (az account show):" -ForegroundColor Yellow
|
|
try {
|
|
az account show | ConvertTo-Json | Out-Host
|
|
} catch {
|
|
Write-Host "Error retrieving Azure CLI context: $($_.Exception.Message)" -ForegroundColor Red
|
|
}
|
|
Write-Host "`n`n"
|
|
}
|
|
|
|
if($OnlyBuild) {
|
|
Write-Host "Just building the test projects, not running tests." -ForegroundColor Yellow
|
|
Invoke-LoggedCommand "dotnet build '$solutionPath' --configuration 'Debug'" -AllowedExitCodes @(0)
|
|
exit $LastExitCode
|
|
}
|
|
|
|
$coverageArg = $CollectCoverage ? "--coverlet --coverlet-output-format cobertura" : ""
|
|
$resultsArg = "--results-directory '$TestResultsPath'"
|
|
$loggerArg = "--report-xunit-trx --output 'Detailed'"
|
|
$filterArg = switch ($TestType) {
|
|
'Live' { "--filter-trait 'TestType=Live'" }
|
|
'Unit' { "--filter-not-trait 'TestType=Live'" }
|
|
'Recorded' { "--filter-trait 'TestType=Live'" }
|
|
default { "" }
|
|
}
|
|
|
|
$command = "dotnet test $coverageArg $resultsArg $loggerArg $filterArg"
|
|
|
|
Invoke-LoggedMsBuildCommand -Command $command -AllowedExitCodes @(0, 1)
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
|
|
$testExitCode = $LastExitCode
|
|
|
|
# Coverage Report Generation - only if coverage collection was enabled
|
|
if ($CollectCoverage) {
|
|
Create-CoverageReport
|
|
}
|
|
|
|
exit $testExitCode
|