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