diff --git a/.gitignore b/.gitignore index 2201e38a3..7eb5eab99 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ package-dev/alias-references.txt # ignore integration test files Samples/IntegrationTest* unity.log +unity-test.log # Ignore package release test-package-release/ diff --git a/Agents.md b/Agents.md index b81241096..9c826366a 100644 --- a/Agents.md +++ b/Agents.md @@ -18,12 +18,19 @@ dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity # Build the Unity SDK dotnet build -# Run all tests -./test.sh +# Run all tests (builds SDK first) +pwsh scripts/run-tests.ps1 -# Run specific test targets -dotnet msbuild /t:UnityEditModeTest /p:Configuration=Release test/Sentry.Unity.Editor.Tests -dotnet msbuild /t:UnityPlayModeTest /p:Configuration=Release +# Run specific test types +pwsh scripts/run-tests.ps1 -PlayMode +pwsh scripts/run-tests.ps1 -EditMode + +# Run filtered tests +pwsh scripts/run-tests.ps1 -Filter "TestClassName" +pwsh scripts/run-tests.ps1 -PlayMode -Filter "Throttler" + +# Skip build for faster iteration +pwsh scripts/run-tests.ps1 -SkipBuild -Filter "MyTest" # Integration testing (local) ./test/Scripts.Integration.Test/integration-test.ps1 -Platform "macOS" -UnityVersion "2021.3.45f2" @@ -482,7 +489,14 @@ Key options: ### Running All Tests ```bash -./test.sh +# Run all tests (builds SDK first) +pwsh scripts/run-tests.ps1 + +# Run with filtering +pwsh scripts/run-tests.ps1 -Filter "TestClassName" + +# Skip build for faster iteration +pwsh scripts/run-tests.ps1 -SkipBuild ``` ### Integration Test Scripts @@ -520,10 +534,20 @@ Supported platforms: `macOS`, `Windows`, `Linux`, `Android`, `iOS`, `WebGL` ### Development Workflow +**Prerequisites (first-time setup or after clean):** +```bash +# Download native SDKs - REQUIRED before building +dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity +``` + +**Development cycle:** 1. Make changes to source code in `src/` 2. Run `dotnet build` to build and update `package-dev/` -3. Test changes using the sample project or integration tests -4. Run `pwsh scripts/repack.ps1` before creating releases +3. Run `pwsh scripts/run-tests.ps1` to build and run all tests +4. Test changes using the sample project or integration tests +5. Run `pwsh scripts/repack.ps1` before creating releases + +> **Note:** The native SDKs in `package-dev/Plugins/` are not committed to the repository. You must run `DownloadNativeSDKs` before the first build or after cleaning the repository. ### Error Handling Patterns diff --git a/Directory.Build.targets b/Directory.Build.targets index 5e471defd..733cfff87 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -398,7 +398,7 @@ Related: https://forum.unity.com/threads/6572-debugger-agent-unable-to-listen-on - + @@ -411,7 +411,7 @@ Related: https://forum.unity.com/threads/6572-debugger-agent-unable-to-listen-on - + @@ -465,104 +465,6 @@ File.WriteAllLines(PackageManifestFile, lines); - - - - - - - - - - - - - 1 ? "s" : "")}."; -Log.LogError(errorMessage); - -Success = false; - -void PrintFailedTests(XElement element) -{ - foreach (var descendant in element.Descendants()) - { - if (descendant.Name != "test-case" - || descendant.Attribute("result")?.Value != "Failed") - { - continue; - } - - if (descendant.Descendants().Any(d => d.Name == "test-case")) - { - PrintFailedTests(descendant); - } - else - { - var sb = new StringBuilder() - .Append("Test ") - .Append(descendant.Attribute("id")?.Value) - .Append(": ") - .AppendLine(descendant.Attribute("name")?.Value); - - var failure = descendant.Descendants("failure") - .Descendants("message") - .FirstOrDefault() - ?.Value; - - var stack = descendant.Descendants("failure") - .Descendants("stack-trace") - .FirstOrDefault() - ?.Value; - - sb.AppendLine(failure) - .Append("Test StackTrace: ") - .AppendLine(stack); - -// MSBuild is breaking each line as if it was an error per line and not a single error. -// So Log.LogError got replaced by Console.WriteLine for now. - Console.WriteLine(sb.ToString()); - } - } -} -]]> - - - - - - - - - - - - - - - - - - - - - - + diff --git a/scripts/download-native-sdks.ps1 b/scripts/download-native-sdks.ps1 new file mode 100644 index 000000000..63c3b9588 --- /dev/null +++ b/scripts/download-native-sdks.ps1 @@ -0,0 +1,136 @@ +#!/usr/bin/env pwsh + +param( + [Parameter()] + [string]$RepoRoot = "$PSScriptRoot/.." +) + +Set-StrictMode -Version latest +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +$ArtifactsDestination = Join-Path $RepoRoot "package-dev/Plugins" + +# SDK definitions with their existence checks +$SDKs = @( + @{ + Name = "Windows" + Destination = Join-Path $ArtifactsDestination "Windows" + CheckFile = "Sentry/sentry.dll" + }, + @{ + Name = "Linux" + Destination = Join-Path $ArtifactsDestination "Linux" + CheckFile = "Sentry/libsentry.so" + }, + @{ + Name = "Android" + Destination = Join-Path $ArtifactsDestination "Android" + CheckDir = "Sentry~" + ExpectedFileCount = 4 + } +) + +function Test-SDKPresent { + param($SDK) + + if ($SDK.ContainsKey('CheckFile')) { + $checkPath = Join-Path $SDK.Destination $SDK.CheckFile + return Test-Path $checkPath + } + elseif ($SDK.ContainsKey('CheckDir')) { + $checkPath = Join-Path $SDK.Destination $SDK.CheckDir + if (-not (Test-Path $checkPath)) { + return $false + } + $fileCount = (Get-ChildItem -Path $checkPath -File).Count + return $fileCount -ge $SDK.ExpectedFileCount + } + return $false +} + +function Get-LatestSuccessfulRunId { + Write-Host "Fetching latest successful CI run ID..." -ForegroundColor Yellow + + $result = gh run list --branch main --workflow CI --json "conclusion,databaseId" --jq 'first(.[] | select(.conclusion == "success") | .databaseId)' + + if (-not $result -or $result -eq "null") { + Write-Error "Failed to find a successful CI run on main branch" + exit 1 + } + + Write-Host "Found run ID: $result" -ForegroundColor Green + return $result +} + +function Download-SDK { + param( + [Parameter(Mandatory)] + [string]$Name, + [Parameter(Mandatory)] + [string]$Destination, + [Parameter(Mandatory)] + [string]$RunId + ) + + Write-Host "Downloading $Name SDK..." -ForegroundColor Yellow + + # Remove existing directory if present (partial download) + if (Test-Path $Destination) { + Write-Host " Removing existing directory..." -ForegroundColor Gray + Remove-Item -Path $Destination -Recurse -Force + } + + $artifactName = "$Name-sdk" + gh run download $RunId -n $artifactName -D $Destination + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to download $Name SDK" + exit 1 + } + + Write-Host " Downloaded $Name SDK successfully" -ForegroundColor Green +} + +# Main logic +Write-Host "Checking native SDK status..." -ForegroundColor Cyan +Write-Host "" + +$sdksToDownload = @() + +foreach ($sdk in $SDKs) { + if (Test-SDKPresent $sdk) { + Write-Host "$($sdk.Name) SDK already present, skipping download." -ForegroundColor Green + } + else { + Write-Host "$($sdk.Name) SDK not found, will download." -ForegroundColor Yellow + $sdksToDownload += $sdk + } +} + +Write-Host "" + +if ($sdksToDownload.Count -eq 0) { + Write-Host "All native SDKs are already present." -ForegroundColor Green + exit 0 +} + +# Fetch run ID only if we need to download something +$runId = Get-LatestSuccessfulRunId + +foreach ($sdk in $sdksToDownload) { + Download-SDK -Name $sdk.Name -Destination $sdk.Destination -RunId $runId +} + +Write-Host "" +Write-Host "Restoring package-dev/Plugins to latest git commit..." -ForegroundColor Yellow +Push-Location $RepoRoot +try { + git restore package-dev/Plugins +} +finally { + Pop-Location +} + +Write-Host "" +Write-Host "Native SDK download completed successfully!" -ForegroundColor Green diff --git a/scripts/report-test-results.ps1 b/scripts/report-test-results.ps1 new file mode 100644 index 000000000..0ab90c9f4 --- /dev/null +++ b/scripts/report-test-results.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS + Reports Unity test results from NUnit XML file. + +.DESCRIPTION + Parses NUnit XML test results and prints a summary. Exits with code 1 if tests failed. + Designed to be called from MSBuild targets. + +.PARAMETER Path + Path to the NUnit XML test results file. + +.EXAMPLE + pwsh scripts/report-test-results.ps1 artifacts/test/playmode/results.xml +#> + +param( + [Parameter(Mandatory = $true, Position = 0)] + [string] $Path +) + +Set-StrictMode -Version latest +$ErrorActionPreference = "Stop" + +. $PSScriptRoot/test-utils.ps1 + +if (-not (Test-Path $Path)) { + Write-Host "Test results file not found at $Path" -ForegroundColor Red + exit 1 +} + +$results = Parse-TestResults $Path + +if ($null -eq $results) { + Write-Host "Failed to parse test results" -ForegroundColor Red + exit 1 +} + +if ($results.Total -eq 0) { + Write-Host "Unity test results is empty." -ForegroundColor Red + exit 1 +} + +# Print summary (matching format from original C# implementation) +$status = if ($results.Success) { "Passed" } else { "Failed" } +Write-Host "$status in $($results.Duration)s" +Write-Host (" Passed: {0,3}" -f $results.Passed) +Write-Host (" Failed: {0,3}" -f $results.Failed) +Write-Host (" Skipped: {0,3}" -f $results.Skipped) +Write-Host (" Inconclusive: {0,3}" -f $results.Inconclusive) + +# Print failed test details +if ($results.Failed -gt 0) { + Write-Host "" + + # Re-parse to get stack traces (not included in Parse-TestResults) + [xml]$xml = Get-Content $Path + $failedNodes = $xml.SelectNodes("//test-case[@result='Failed']") + + foreach ($node in $failedNodes) { + # Skip parent test-cases that contain child test-cases + if ($node.SelectNodes(".//test-case").Count -gt 0) { + continue + } + + $name = $node.GetAttribute("name") + $id = $node.GetAttribute("id") + Write-Host "Test $id`: $name" + + $message = $node.SelectSingleNode("failure/message") + if ($message) { + Write-Host $message.InnerText + } + + $stackTrace = $node.SelectSingleNode("failure/stack-trace") + if ($stackTrace) { + Write-Host "Test StackTrace:" + Write-Host $stackTrace.InnerText + } + + Write-Host "" + } + + $testWord = if ($results.Failed -gt 1) { "tests" } else { "test" } + Write-Host "Test run completed with $($results.Failed) failing $testWord." -ForegroundColor Red +} + +# Exit based on overall success (handles edge cases where result != "Passed" but failed count is 0) +if (-not $results.Success) { + exit 1 +} +exit 0 diff --git a/scripts/run-tests.ps1 b/scripts/run-tests.ps1 new file mode 100644 index 000000000..df3baffe7 --- /dev/null +++ b/scripts/run-tests.ps1 @@ -0,0 +1,215 @@ +<# +.SYNOPSIS + Runs Unity tests for the Sentry SDK for Unity. + +.DESCRIPTION + This script builds the SDK and runs PlayMode and/or EditMode tests with optional filtering. + +.PARAMETER PlayMode + Run PlayMode tests (runtime tests). + +.PARAMETER EditMode + Run EditMode tests (editor tests). + +.PARAMETER Filter + Test name filter passed to Unity's -testFilter (regex supported). + +.PARAMETER Category + Test category filter passed to Unity's -testCategory. + +.PARAMETER UnityVersion + Override Unity version (default: read from ProjectVersion.txt or $env:UNITY_VERSION). + +.PARAMETER SkipBuild + Skip the dotnet build step (for faster iteration when DLLs are current). + +.EXAMPLE + pwsh scripts/run-tests.ps1 + # Runs all tests (PlayMode + EditMode) + +.EXAMPLE + pwsh scripts/run-tests.ps1 -PlayMode -Filter "Throttler" + # Runs only PlayMode tests matching "Throttler" + +.EXAMPLE + pwsh scripts/run-tests.ps1 -SkipBuild -EditMode + # Runs EditMode tests without rebuilding +#> + +param( + [switch] $PlayMode, + [switch] $EditMode, + [string] $Filter, + [string] $Category, + [string] $UnityVersion, + [switch] $SkipBuild +) + +Set-StrictMode -Version latest +$ErrorActionPreference = "Stop" + +. $PSScriptRoot/test-utils.ps1 + +$repoRoot = Resolve-Path "$PSScriptRoot/.." +$sampleProject = "$repoRoot/samples/unity-of-bugs" + +# Default to both if neither specified +if (-not $PlayMode -and -not $EditMode) { + $PlayMode = $true + $EditMode = $true +} + +# Find Unity version +if (-not $UnityVersion) { + $UnityVersion = $env:UNITY_VERSION + if (-not $UnityVersion) { + $projectVersionFile = "$sampleProject/ProjectSettings/ProjectVersion.txt" + if (-not (Test-Path $projectVersionFile)) { + Write-Host "Error: ProjectVersion.txt not found at $projectVersionFile" -ForegroundColor Red + exit 1 + } + $content = Get-Content $projectVersionFile -Raw + $match = [regex]::Match($content, "m_EditorVersion:\s*(.+)") + if (-not $match.Success) { + Write-Host "Error: Could not parse Unity version from ProjectVersion.txt" -ForegroundColor Red + exit 1 + } + $UnityVersion = $match.Groups[1].Value.Trim() + } +} +# Find Unity path (platform-specific) +if ($IsMacOS) { + $unityPath = "/Applications/Unity/Hub/Editor/$UnityVersion/Unity.app/Contents/MacOS/Unity" +} +elseif ($IsWindows) { + $unityPath = "C:/Program Files/Unity/Hub/Editor/$UnityVersion/Editor/Unity.exe" +} +elseif ($IsLinux) { + $unityPath = "$env:HOME/Unity/Hub/Editor/$UnityVersion/Editor/Unity" +} +else { + Write-Host "Error: Unsupported platform" -ForegroundColor Red + exit 1 +} + +if (-not (Test-Path $unityPath)) { + Write-Host "Error: Unity $UnityVersion not found at: $unityPath" -ForegroundColor Red + exit 1 +} + +# Build SDK +if (-not $SkipBuild) { + Write-Host "Building SDK... " -NoNewline + $buildStart = Get-Date + & dotnet build "$repoRoot" --configuration Release --verbosity quiet 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "[FAIL]" -ForegroundColor Red + exit 1 + } + $buildTime = (Get-Date) - $buildStart + Write-Host "[OK] ($([math]::Round($buildTime.TotalSeconds, 1))s)" -ForegroundColor Green +} + +# Build test arguments +function Build-TestArgs([string] $testPlatform, [string] $resultsPath) { + $testArgs = @( + "-batchmode", "-nographics", "-runTests", + "-testPlatform", $testPlatform, + "-projectPath", $sampleProject, + "-testResults", $resultsPath + ) + if ($Filter) { $testArgs += @("-testFilter", $Filter) } + if ($Category) { $testArgs += @("-testCategory", $Category) } + return $testArgs +} + +# Run Unity quietly and return success/failure +function Run-Unity-Quiet([string] $unityExe, [string[]] $arguments) { + $logFile = "$repoRoot/unity-test.log" + $arguments += @("-logFile", $logFile) + + # Remove old log + Remove-Item $logFile -ErrorAction SilentlyContinue + + # Run Unity and wait for completion + $process = Start-Process -FilePath $unityExe -ArgumentList $arguments -PassThru + $process | Wait-Process + + return $process.ExitCode +} + +# Run tests and report results +function Run-Tests([string] $name, [string] $platform, [string] $resultsPath) { + $filterInfo = if ($Filter) { " (filter: `"$Filter`")" } else { "" } + Write-Host "Running $name tests$filterInfo... " -NoNewline + + # Ensure results directory exists + $resultsDir = Split-Path $resultsPath -Parent + if (-not (Test-Path $resultsDir)) { + New-Item -ItemType Directory -Path $resultsDir -Force | Out-Null + } + + # Remove old results + Remove-Item $resultsPath -ErrorAction SilentlyContinue + + # Build and run Unity quietly + $testArgs = Build-TestArgs $platform $resultsPath + $null = Run-Unity-Quiet $unityPath $testArgs + + # Parse and display results + $results = Parse-TestResults $resultsPath + if ($null -eq $results) { + Write-Host "[FAIL] Could not parse test results" -ForegroundColor Red + Write-Host " Check unity-test.log for details" -ForegroundColor DarkGray + return $false + } + + if ($results.Total -eq 0) { + Write-Host "[WARN] No tests found" -ForegroundColor Yellow + return $true + } + + $symbol = if ($results.Success) { "[PASS]" } else { "[FAIL]" } + $color = if ($results.Success) { "Green" } else { "Red" } + $duration = [math]::Round($results.Duration, 1) + + Write-Host "$symbol $($results.Passed) passed, $($results.Failed) failed, $($results.Inconclusive) inconclusive ($duration`s)" -ForegroundColor $color + + # Show failed test details + if ($results.Failed -gt 0) { + Write-Host "" + foreach ($test in $results.FailedTests) { + Write-Host " FAILED: $($test.Name)" -ForegroundColor Red + if ($test.Message) { + $test.Message.Trim() -split "`n" | Select-Object -First 3 | ForEach-Object { + Write-Host " $($_.Trim())" -ForegroundColor DarkGray + } + } + } + } + + return $results.Success +} + +# Run requested tests +$allPassed = $true + +if ($PlayMode) { + $result = Run-Tests "PlayMode" "PlayMode" "$repoRoot/artifacts/test/playmode/results.xml" + if ($result -ne $true) { $allPassed = $false } +} + +if ($EditMode) { + $result = Run-Tests "EditMode" "EditMode" "$repoRoot/artifacts/test/editmode/results.xml" + if ($result -ne $true) { $allPassed = $false } +} + +# Final summary +if ($allPassed) { + Write-Host "`nAll tests passed." -ForegroundColor Green + exit 0 +} +else { + Write-Host "`nTests failed." -ForegroundColor Red + exit 1 +} diff --git a/scripts/test-utils.ps1 b/scripts/test-utils.ps1 new file mode 100644 index 000000000..348dca7b0 --- /dev/null +++ b/scripts/test-utils.ps1 @@ -0,0 +1,69 @@ +# Shared test utilities for Unity test result parsing + +<# +.SYNOPSIS + Parses NUnit XML test results from Unity test runner. + +.DESCRIPTION + Reads an NUnit XML test results file and returns a hashtable with test statistics + and details about failed tests. + +.PARAMETER Path + Path to the NUnit XML test results file. + +.OUTPUTS + Hashtable with: Total, Passed, Failed, Inconclusive, Skipped, Duration, Success, FailedTests + Returns $null if the file doesn't exist or cannot be parsed. + +.EXAMPLE + $results = Parse-TestResults "artifacts/test/playmode/results.xml" + if ($results.Success) { Write-Host "All tests passed!" } +#> +function Parse-TestResults([string] $Path) { + if (-not (Test-Path $Path)) { + return $null + } + + try { + [xml]$xml = Get-Content $Path + } + catch { + Write-Host " Failed to parse XML: $_" -ForegroundColor Red + return $null + } + + $testRun = $xml.'test-run' + + if ($null -eq $testRun) { + Write-Host " Invalid test results XML" -ForegroundColor Red + return $null + } + + $result = @{ + Total = [int]$testRun.total + Passed = [int]$testRun.passed + Failed = [int]$testRun.failed + Inconclusive = [int]$testRun.inconclusive + Skipped = [int]$testRun.skipped + Duration = [double]$testRun.duration + Success = $testRun.result -eq "Passed" + FailedTests = @() + } + + # Collect failed test details + if ($result.Failed -gt 0) { + $failedNodes = $xml.SelectNodes("//test-case[@result='Failed']") + $result.FailedTests = @($failedNodes | ForEach-Object { + $msg = $null + if ($_.failure -and $_.failure.message) { + $msg = $_.failure.message.InnerText + } + @{ + Name = $_.fullname + Message = $msg + } + }) + } + + return $result +} diff --git a/test.sh b/test.sh deleted file mode 100755 index c87c907d0..000000000 --- a/test.sh +++ /dev/null @@ -1,3 +0,0 @@ -dotnet msbuild /t:UnityEditModeTest /p:Configuration=Release -dotnet msbuild /t:UnityPlayModeTest /p:Configuration=Release -dotnet msbuild /t:UnitySmokeTestStandalonePlayerIL2CPP /p:Configuration=Release