diff --git a/.github/instructions/pwsh.instructions.md b/.github/instructions/pwsh.instructions.md new file mode 100644 index 00000000000..1a03b63e894 --- /dev/null +++ b/.github/instructions/pwsh.instructions.md @@ -0,0 +1,84 @@ +--- +applyTo: "**/*.ps1" +--- + +# PowerShell Script Instructions + +## Path Handling + +- **Always use `[System.IO.Path]::Combine` instead of `Join-Path`** + - Use the full form: `([System.IO.Path]::Combine($path1, $path2, ...))` + - Example: `([System.IO.Path]::Combine($PSScriptRoot, '..', 'common', 'scripts', 'common.ps1'))` + - This ensures consistent path handling across all platforms + +## Command Invocation + +- **Use `Invoke-LoggedCommand` for invoking external commands** + - Commands should be logged unless they output to stderr that needs to be processed + - Use `-GroupOutput` to collapse output in CI logs + - Use `-DoNotExitOnFailedExitCode` when you need to handle exit codes manually + - Example: `Invoke-LoggedCommand "cargo build --keep-going" -GroupOutput` + - Example with error handling: `Invoke-LoggedCommand "cargo test ..." -GroupOutput -DoNotExitOnFailedExitCode` + +## Output Grouping + +- **Use `LogGroupStart` and `LogGroupEnd` to collapse verbose output** + - Use for output that is verbose or not relevant in most cases (e.g., large JSON dumps, detailed test output) + - Example: + ```powershell + LogGroupStart "Raw JSON Output" + Get-Content $jsonFile | ForEach-Object { Write-Host $_ } + LogGroupEnd + ``` + +## Console Output + +- **Do not use emojis in console output** + - Use plain text instead + - Bad: `Write-Host "✅ Tests passed"` + - Good: `Write-Host "Tests passed"` + +## Code Organization + +- **Extract common code patterns into functions** + - If you see similar code blocks repeated, extract them into a reusable function + - Functions should have clear parameters and return values + - Example: Extract test execution logic into a shared function rather than duplicating it + +## Parameter Defaults + +- **Use default parameter values when sensible defaults are available** + - Set default values directly in the parameter declaration instead of checking and setting in the script body + - Example: + ```powershell + param( + [string]$OutputDirectory = ([System.IO.Path]::Combine($PSScriptRoot, '..', 'output')) + ) + ``` + +## Error Handling + +- **Use `LogError` for error messages if common.ps1 is imported** + - If you've imported common.ps1, use `LogError` followed by `exit 1` + - If you're not sure if common.ps1 is imported, use `Write-Host` with red color + - Never use `Write-Error` + - Example with common.ps1: + ```powershell + if ($exitCode -ne 0) { + LogError "Operation failed" + exit 1 + } + ``` + - Example without common.ps1: + ```powershell + if ($exitCode -ne 0) { + Write-Host "Operation failed" -ForegroundColor Red + exit 1 + } + ``` +- **Use `LogWarning` for warning messages if common.ps1 is imported** + - If you've imported common.ps1, use `LogWarning` instead of `Write-Warning` + - Example: `LogWarning "Test results directory not found"` +- **Fail fast on errors** + - Exit immediately when a critical operation fails + - Unless otherwise specified, don't accumulate errors and summarize at the end diff --git a/.gitignore b/.gitignore index d6e75923ad7..97630422509 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ vcpkg_installed/ # Temporary folder to refresh SDK with TypeSpec. TempTypeSpecFiles/ +test-results/ diff --git a/eng/cgmanifest.json b/eng/cgmanifest.json index 538e25da889..b4969252667 100644 --- a/eng/cgmanifest.json +++ b/eng/cgmanifest.json @@ -2,6 +2,12 @@ "$schema": "https://json.schemastore.org/component-detection-manifest.json", "version": 1, "registrations": [ + { + "component": { + "type": "cargo", + "cargo": { "name": "cargo2junit", "version": "0.1.14" } + } + }, { "component": { "type": "cargo", diff --git a/eng/pipelines/templates/jobs/ci.tests.yml b/eng/pipelines/templates/jobs/ci.tests.yml index ec6ec822407..5339858cb02 100644 --- a/eng/pipelines/templates/jobs/ci.tests.yml +++ b/eng/pipelines/templates/jobs/ci.tests.yml @@ -73,6 +73,23 @@ jobs: arguments: > -PackageInfoDirectory '$(Build.ArtifactStagingDirectory)/PackageInfo' + - task: Powershell@2 + displayName: "Convert Test Results to JUnit XML" + condition: and(succeededOrFailed(), ne(variables['NoPackagesChanged'],'true')) + inputs: + pwsh: true + filePath: $(Build.SourcesDirectory)/eng/scripts/Convert-TestResultsToJUnit.ps1 + + - task: PublishTestResults@2 + displayName: "Publish Test Results" + condition: and(succeededOrFailed(), ne(variables['NoPackagesChanged'],'true')) + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/test-results/junit/*.xml' + testRunTitle: 'Rust Tests - $(Agent.JobName)' + mergeTestResults: true + failTaskOnFailedTests: false + - ${{ if eq(parameters.TestProxy, true) }}: - pwsh: | # $(Build.SourcesDirectory)/test-proxy.log is the hardcoded output log location for the test-proxy-tool.yml diff --git a/eng/scripts/Convert-TestResultsToJUnit.ps1 b/eng/scripts/Convert-TestResultsToJUnit.ps1 new file mode 100644 index 00000000000..8c2204fb909 --- /dev/null +++ b/eng/scripts/Convert-TestResultsToJUnit.ps1 @@ -0,0 +1,118 @@ +#!/usr/bin/env pwsh + +#Requires -Version 7.0 +<# +.SYNOPSIS +Converts cargo test JSON output to JUnit XML format using cargo2junit. + +.DESCRIPTION +This script converts the JSON output files from cargo test (captured by Test-Packages.ps1 in CI mode) +to JUnit XML format suitable for publishing to Azure DevOps test results using the cargo2junit tool. + +.PARAMETER TestResultsDirectory +The directory containing JSON test result files. Defaults to test-results in the repo root. + +.PARAMETER OutputDirectory +The directory where JUnit XML files should be written. Defaults to test-results/junit in the repo root. + +.EXAMPLE +./eng/scripts/Convert-TestResultsToJUnit.ps1 + +.EXAMPLE +./eng/scripts/Convert-TestResultsToJUnit.ps1 -TestResultsDirectory ./test-results -OutputDirectory ./junit-results +#> + +param( + [string]$TestResultsDirectory, + [string]$OutputDirectory +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 2.0 +. ([System.IO.Path]::Combine($PSScriptRoot, '..', 'common', 'scripts', 'common.ps1')) + +# Set default directories (must be after sourcing common.ps1 which defines $RepoRoot) +if (!$TestResultsDirectory) { + $TestResultsDirectory = ([System.IO.Path]::Combine($RepoRoot, 'test-results')) +} + +if (!$OutputDirectory) { + $OutputDirectory = ([System.IO.Path]::Combine($RepoRoot, 'test-results', 'junit')) +} + +Write-Host "Converting test results from JSON to JUnit XML using cargo2junit" +Write-Host " Input directory: $TestResultsDirectory" +Write-Host " Output directory: $OutputDirectory" + +# Check if test results directory exists +if (!(Test-Path $TestResultsDirectory)) { + LogWarning "Test results directory not found: $TestResultsDirectory" + Write-Host "No test results to convert." + exit 0 +} + +# Create output directory if it doesn't exist +if (!(Test-Path $OutputDirectory)) { + New-Item -ItemType Directory -Path $OutputDirectory | Out-Null + Write-Host "Created output directory: $OutputDirectory" +} + +# Check if cargo2junit is installed +$cargo2junitPath = Get-Command cargo2junit -ErrorAction SilentlyContinue +if (!$cargo2junitPath) { + Write-Host "cargo2junit not found. Installing..." + Invoke-LoggedCommand "cargo install cargo2junit" -GroupOutput +} + +# Get all JSON files in the test results directory +$jsonFiles = @(Get-ChildItem -Path $TestResultsDirectory -Filter "*.json" -File) + +if ($jsonFiles.Count -eq 0) { + LogWarning "No JSON files found in $TestResultsDirectory" + Write-Host "No test results to convert." + exit 0 +} + +Write-Host "`nConverting $($jsonFiles.Count) JSON file(s) to JUnit XML..." + +$convertedCount = 0 +$failedCount = 0 + +foreach ($jsonFile in $jsonFiles) { + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($jsonFile.Name) + $junitFile = ([System.IO.Path]::Combine($OutputDirectory, "$baseName.xml")) + + Write-Host " Converting: $($jsonFile.Name) -> $([System.IO.Path]::GetFileName($junitFile))" + + try { + # Convert JSON to JUnit XML using cargo2junit + Get-Content $jsonFile.FullName | cargo2junit > $junitFile + + if ($LASTEXITCODE -ne 0) { + LogWarning " cargo2junit returned exit code $LASTEXITCODE for $($jsonFile.Name)" + $failedCount++ + } + else { + $convertedCount++ + } + } + catch { + LogWarning " Failed to convert $($jsonFile.Name): $_" + $failedCount++ + } +} + +Write-Host "`nConversion complete:" +Write-Host " Successfully converted: $convertedCount" +if ($failedCount -gt 0) { + Write-Host " Failed to convert: $failedCount" -ForegroundColor Yellow +} + +Write-Host "`nJUnit XML files are available in: $OutputDirectory" + +# Exit with error if any conversions failed +if ($failedCount -gt 0) { + exit 1 +} + +exit 0 diff --git a/eng/scripts/Test-Packages.ps1 b/eng/scripts/Test-Packages.ps1 index a9515d9f8dd..62df0b735df 100755 --- a/eng/scripts/Test-Packages.ps1 +++ b/eng/scripts/Test-Packages.ps1 @@ -9,6 +9,99 @@ $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 . "$PSScriptRoot/../common/scripts/common.ps1" +# Helper function to parse test results from JSON and output human-readable summary +function Write-TestSummary { + param( + [string]$JsonFile, + [string]$PackageName + ) + + if (!(Test-Path $JsonFile)) { + Write-Warning "Test results file not found: $JsonFile" + return + } + + $passed = 0 + $failed = 0 + $ignored = 0 + $failedTests = @() + + # Parse JSON output (newline-delimited JSON) + Get-Content $JsonFile | ForEach-Object { + try { + $event = $_ | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($event.type -eq "test" -and $event.event) { + switch ($event.event) { + "ok" { $passed++ } + "failed" { + $failed++ + $failedTests += $event.name + } + "ignored" { $ignored++ } + } + } + } + catch { + # Ignore lines that aren't valid JSON + } + } + + LogGroupStart "Test Summary: $PackageName" + Write-Host "Passed: $passed" -ForegroundColor Green + Write-Host "Failed: $failed" -ForegroundColor $(if ($failed -gt 0) { "Red" } else { "Green" }) + Write-Host "Ignored: $ignored" -ForegroundColor Yellow + + if ($failed -gt 0) { + Write-Host "`nFailed tests:" -ForegroundColor Red + foreach ($test in $failedTests) { + Write-Host " - $test" -ForegroundColor Red + } + Write-Host "`nAdditional details are available in the test tab for the build." -ForegroundColor Yellow + } + LogGroupEnd + + return @{ + Passed = $passed + Failed = $failed + Ignored = $ignored + } +} + +# Helper function to run cargo test with JSON output +function Invoke-CargoTestWithJsonOutput { + param( + [string]$TestParams, + [string]$TestDescription, + [string]$OutputFile + ) + + Write-Host "Running $TestDescription with JSON output to: $OutputFile" + + # Use cargo +nightly test with --format json and -Z unstable-options + $testCommand = "cargo +nightly test $TestParams --no-fail-fast -- --format json -Z unstable-options" + + # Run the test command and capture output + $result = Invoke-LoggedCommand $testCommand -GroupOutput -DoNotExitOnFailedExitCode + + LogGroupStart 'Test result JSON' + $result | Write-Host + LogGroupEnd + + # Write JSON to file + $result | Out-File -FilePath $OutputFile -Encoding utf8 + + # Parse and display summary + $results = Write-TestSummary -JsonFile $OutputFile -PackageName $TestDescription + + # Exit immediately if tests failed + if ($LASTEXITCODE -ne 0) { + LogError "Tests failed for $TestDescription" + exit $LASTEXITCODE + } + + return $results +} + Write-Host @" Testing packages with PackageInfoDirectory: '$PackageInfoDirectory' @@ -20,9 +113,16 @@ Testing packages with ARM_OIDC_TOKEN: $($env:ARM_OIDC_TOKEN ? 'present' : 'not present') "@ +# Create directory for test results +$testResultsDir = ([System.IO.Path]::Combine($RepoRoot, 'test-results')) +if (!(Test-Path $testResultsDir)) { + New-Item -ItemType Directory -Path $testResultsDir | Out-Null +} +Write-Host "Test results will be saved to: $testResultsDir" + if ($PackageInfoDirectory) { if (!(Test-Path $PackageInfoDirectory)) { - Write-Error "Package info path '$PackageInfoDirectory' does not exist." + LogError "Package info path '$PackageInfoDirectory' does not exist." exit 1 } @@ -44,12 +144,12 @@ foreach ($package in $packagesToTest) { try { $packageDirectory = ([System.IO.Path]::Combine($RepoRoot, $package.DirectoryPath)) - $setupScript = Join-Path $packageDirectory "Test-Setup.ps1" + $setupScript = ([System.IO.Path]::Combine($packageDirectory, 'Test-Setup.ps1')) if (Test-Path $setupScript) { Write-Host "`n`nRunning test setup script for package: '$($package.Name)'`n" Invoke-LoggedCommand $setupScript -GroupOutput if (!$? -ne 0) { - Write-Error "Test setup script failed for package: '$($package.Name)'" + LogError "Test setup script failed for package: '$($package.Name)'" exit 1 } } @@ -59,13 +159,19 @@ foreach ($package in $packagesToTest) { Invoke-LoggedCommand "cargo build --keep-going" -GroupOutput Write-Host "`n`n" - Invoke-LoggedCommand "cargo test --doc --no-fail-fast" -GroupOutput - Write-Host "`n`n" - - Invoke-LoggedCommand "cargo test --all-targets --no-fail-fast" -GroupOutput - Write-Host "`n`n" + # Generate unique filenames for test outputs + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss-fff" + $sanitizedPackageName = $package.Name -replace '[^a-zA-Z0-9_-]', '_' + + # Run doc tests + $docTestOutput = ([System.IO.Path]::Combine($testResultsDir, "$sanitizedPackageName-doctest-$timestamp.json")) + Invoke-CargoTestWithJsonOutput -TestParams "--doc" -TestDescription "$($package.Name) (doc tests)" -OutputFile $docTestOutput + + # Run all-targets tests + $allTargetsOutput = ([System.IO.Path]::Combine($testResultsDir, "$sanitizedPackageName-alltargets-$timestamp.json")) + Invoke-CargoTestWithJsonOutput -TestParams "--all-targets" -TestDescription "$($package.Name) (all targets)" -OutputFile $allTargetsOutput - $cleanupScript = Join-Path $packageDirectory "Test-Cleanup.ps1" + $cleanupScript = ([System.IO.Path]::Combine($packageDirectory, 'Test-Cleanup.ps1')) if (Test-Path $cleanupScript) { Write-Host "`n`nRunning test cleanup script for package: '$($package.Name)'`n" Invoke-LoggedCommand $cleanupScript -GroupOutput diff --git a/sdk/canary/azure_canary_core/src/lib.rs b/sdk/canary/azure_canary_core/src/lib.rs index aa702d75587..15fc8b5672a 100644 --- a/sdk/canary/azure_canary_core/src/lib.rs +++ b/sdk/canary/azure_canary_core/src/lib.rs @@ -43,4 +43,9 @@ mod tests { let result = add(2, 2); assert_eq!(result, 4); } + + #[test] + fn fails() { + assert_eq!(1, 0); + } }