Extend gate to support all test types (UI, device, unit, XAML)#34657
Extend gate to support all test types (UI, device, unit, XAML)#34657
Conversation
- verify-tests-fail.ps1: auto-detect test type from changed files, route to correct runner (BuildAndRunHostApp, Run-DeviceTests, dotnet test) - Detect-TestsInDiff.ps1: shared detection script for PR test analysis - pr-gate.md: use detection script instead of grep for TestCases only - EstablishBrokenBaseline.ps1: exclude TestUtils/DeviceTests.Runners from fix files - RunTests.ps1: unified test runner entry point - Platform is now mandatory for all test types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34657Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34657" |
|
Changes already on copilot-kubaflo branch which the pipeline uses |
There was a problem hiding this comment.
Pull request overview
Extends the PR “gate” workflow (verify-tests-fail-without-fix) to auto-detect and run multiple test types (UI, device, unit, XAML) by introducing a shared diff/PR test detection script and updating gate documentation accordingly.
Changes:
- Added a shared test detection script (
Detect-TestsInDiff.ps1) that classifies changed tests and provides runner/filter metadata. - Updated
verify-tests-fail.ps1to iterate over all detected tests and dispatch to the correct runner per test type. - Added a unified local runner entry point (
RunTests.ps1) and updated gate docs (pr-gate.md, skill docs) to use the new detection approach.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 | Routes execution by detected test type and runs all detected tests across both “without fix” and “with fix” runs. |
| .github/skills/verify-tests-fail-without-fix/SKILL.md | Documents multi-test-type support and updated usage guidance. |
| .github/scripts/shared/Detect-TestsInDiff.ps1 | New shared detector that classifies tests from PR files/git diff and returns structured entries. |
| .github/scripts/RunTests.ps1 | New unified local test runner wrapper for unit/device/UI/integration tests. |
| .github/scripts/EstablishBrokenBaseline.ps1 | Expands “test infrastructure” patterns to avoid misclassifying infra changes as fix files. |
| .github/pr-review/pr-gate.md | Updates gate instructions to use the shared detection script and reflect multi-type gating. |
| # Maps file path patterns to test types | ||
| $script:TestTypePatterns = @( | ||
| @{ Pattern = "TestCases\.(Shared\.Tests|HostApp|Android\.Tests|iOS\.Tests|Mac\.Tests|WinUI\.Tests)"; Type = "UITest" } | ||
| @{ Pattern = "Xaml\.UnitTests/"; Type = "XamlUnitTest" } | ||
| @{ Pattern = "DeviceTests/"; Type = "DeviceTest" } | ||
| @{ Pattern = "(?<!\w)UnitTests/|Graphics\.Tests/"; Type = "UnitTest" } | ||
| ) | ||
|
|
||
| # Maps test types to their project paths (relative to repo root) | ||
| $script:UnitTestProjectMap = @{ | ||
| "Controls.Core.UnitTests" = "src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj" | ||
| "Controls.Xaml.UnitTests" = "src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj" | ||
| "Controls.BindingSourceGen.UnitTests" = "src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj" | ||
| "SourceGen.UnitTests" = "src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj" | ||
| "Core.UnitTests" = "src/Core/tests/UnitTests/Core.UnitTests.csproj" | ||
| "Essentials.UnitTests" = "src/Essentials/test/UnitTests/Essentials.UnitTests.csproj" | ||
| "Graphics.Tests" = "src/Graphics/tests/Graphics.Tests/Graphics.Tests.csproj" | ||
| "Resizetizer.UnitTests" = "src/SingleProject/Resizetizer/test/UnitTests/Resizetizer.UnitTests.csproj" | ||
| "Compatibility.Core.UnitTests" = "src/Compatibility/Core/tests/Compatibility.UnitTests/Compatibility.Core.UnitTests.csproj" | ||
| "Essentials.AI.UnitTests" = "src/AI/tests/Essentials.AI.UnitTests/Essentials.AI.UnitTests.csproj" | ||
| } | ||
|
|
There was a problem hiding this comment.
$TestTypePatterns, $UnitTestProjectMap, $DeviceTestProjectMap, and Get-TestTypeFromFiles appear to be unused now that detection is delegated to .github/scripts/shared/Detect-TestsInDiff.ps1 (nothing calls Get-TestTypeFromFiles). Keeping two independent detection implementations increases drift risk; consider removing this unused block or wiring the script to use it consistently.
| # Maps file path patterns to test types | |
| $script:TestTypePatterns = @( | |
| @{ Pattern = "TestCases\.(Shared\.Tests|HostApp|Android\.Tests|iOS\.Tests|Mac\.Tests|WinUI\.Tests)"; Type = "UITest" } | |
| @{ Pattern = "Xaml\.UnitTests/"; Type = "XamlUnitTest" } | |
| @{ Pattern = "DeviceTests/"; Type = "DeviceTest" } | |
| @{ Pattern = "(?<!\w)UnitTests/|Graphics\.Tests/"; Type = "UnitTest" } | |
| ) | |
| # Maps test types to their project paths (relative to repo root) | |
| $script:UnitTestProjectMap = @{ | |
| "Controls.Core.UnitTests" = "src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj" | |
| "Controls.Xaml.UnitTests" = "src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj" | |
| "Controls.BindingSourceGen.UnitTests" = "src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj" | |
| "SourceGen.UnitTests" = "src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj" | |
| "Core.UnitTests" = "src/Core/tests/UnitTests/Core.UnitTests.csproj" | |
| "Essentials.UnitTests" = "src/Essentials/test/UnitTests/Essentials.UnitTests.csproj" | |
| "Graphics.Tests" = "src/Graphics/tests/Graphics.Tests/Graphics.Tests.csproj" | |
| "Resizetizer.UnitTests" = "src/SingleProject/Resizetizer/test/UnitTests/Resizetizer.UnitTests.csproj" | |
| "Compatibility.Core.UnitTests" = "src/Compatibility/Core/tests/Compatibility.UnitTests/Compatibility.Core.UnitTests.csproj" | |
| "Essentials.AI.UnitTests" = "src/AI/tests/Essentials.AI.UnitTests/Essentials.AI.UnitTests.csproj" | |
| } |
| - Type: UITest | UnitTest | XamlUnitTest | DeviceTest | ||
| - TestName: Human-readable test name (class name or method name) | ||
| - Filter: dotnet test --filter value | ||
| - Project: Project key for device tests (Controls, Core, etc.) | ||
| - ProjectPath: Relative .csproj path for unit tests | ||
| - Runner: Which script runs it (BuildAndRunHostApp, dotnet-test, Run-DeviceTests) | ||
| - Platform: Whether -Platform is required | ||
| - Files: List of test files |
There was a problem hiding this comment.
The .OUTPUTS block says the script returns a Platform field indicating whether -Platform is required, but the actual objects use NeedsPlatform (and no Platform key). Please update the docs to match the real output shape so callers don’t rely on a non-existent property.
| - Type: UITest | UnitTest | XamlUnitTest | DeviceTest | |
| - TestName: Human-readable test name (class name or method name) | |
| - Filter: dotnet test --filter value | |
| - Project: Project key for device tests (Controls, Core, etc.) | |
| - ProjectPath: Relative .csproj path for unit tests | |
| - Runner: Which script runs it (BuildAndRunHostApp, dotnet-test, Run-DeviceTests) | |
| - Platform: Whether -Platform is required | |
| - Files: List of test files | |
| - Type: UITest | UnitTest | XamlUnitTest | DeviceTest | |
| - TestName: Human-readable test name (class name or method name) | |
| - Filter: dotnet test --filter value | |
| - Project: Project key for device tests (Controls, Core, etc.) | |
| - ProjectPath: Relative .csproj path for unit tests | |
| - Runner: Which script runs it (BuildAndRunHostApp, dotnet-test, Run-DeviceTests) | |
| - NeedsPlatform: Whether -Platform is required | |
| - Files: List of test files |
| $testArgs = @( | ||
| "test", $projectPath, | ||
| "--configuration", $Config, | ||
| "--no-restore", |
There was a problem hiding this comment.
Invoke-UnitTests runs dotnet test with --no-restore, which makes the new unified runner fail on a clean checkout (no prior restore). Unless you’re restoring earlier in this script, consider removing --no-restore or adding a -NoRestore/-Restore switch so default usage works out of the box.
| "--no-restore", |
| --- | ||
| name: verify-tests-fail-without-fix | ||
| description: Verifies UI tests catch the bug. Supports two modes - verify failure only (test creation) or full verification (test + fix validation). | ||
| description: Verifies tests catch the bug. Auto-detects test type (UI tests, device tests, unit tests) and dispatches to the appropriate runner. Supports two modes - verify failure only (test creation) or full verification (test + fix validation). |
There was a problem hiding this comment.
The skill frontmatter description mentions auto-detecting UI/device/unit tests, but this PR also adds XAML unit test support. Consider updating the description to explicitly include XAML unit tests for accuracy/consistency with the Supported Test Types table below.
| description: Verifies tests catch the bug. Auto-detects test type (UI tests, device tests, unit tests) and dispatches to the appropriate runner. Supports two modes - verify failure only (test creation) or full verification (test + fix validation). | |
| description: Verifies tests catch the bug. Auto-detects test type (UI tests, device tests, unit tests, XAML unit tests) and dispatches to the appropriate runner. Supports two modes - verify failure only (test creation) or full verification (test + fix validation). |
| .PARAMETER Platform | ||
| Target platform: "android", "ios", "catalyst" (MacCatalyst), or "windows" | ||
| Required for UITest and DeviceTest types. Optional for UnitTest and XamlUnitTest. | ||
|
|
There was a problem hiding this comment.
The script header says -Platform is optional for UnitTest/XamlUnitTest, but the script’s param(...) still declares Platform as mandatory. As a result, the documented “no platform needed” scenarios won’t work unless Platform is made optional (with runtime validation only for UI/Device tests) or the docs are updated to say it’s always required.
| $TestLog = Join-Path $OutputPath "test-failure-$($testEntry.TestName).log" | ||
|
|
There was a problem hiding this comment.
$testEntry.TestName is used directly in the log file name. When -TestFilter is provided explicitly, TestName is set to the raw filter (e.g. Issue32030|ButtonUITests per docs), which contains characters like | that are invalid in Windows file names. Please sanitize/slugify the value used for file names (keep the original for display).
| $TestLog = Join-Path $OutputPath "test-failure-$($testEntry.TestName).log" | |
| # Sanitize test name for use in file system paths (keep original for display and reporting) | |
| $invalidFileNameChars = [IO.Path]::GetInvalidFileNameChars() | |
| $escapedInvalidFileNameChars = [regex]::Escape(-join $invalidFileNameChars) | |
| $sanitizedTestName = $testEntry.TestName -replace "[${escapedInvalidFileNameChars}]", "_" | |
| $TestLog = Join-Path $OutputPath "test-failure-$sanitizedTestName.log" |
|
|
||
| $testLogFile = Join-Path $OutputPath "test-without-fix-$($testEntry.TestName).log" | ||
|
|
There was a problem hiding this comment.
Same issue as above: $testEntry.TestName is used in test-without-fix-<name>.log. If the test name comes from an explicit -TestFilter (or includes characters like |, :, /), this will fail on Windows. Sanitize the name used for the file path.
| Write-Log "" | ||
| Write-Log "--- Test $testIndex/$($AllDetectedTests.Count): $icon [$($testEntry.Type)] $($testEntry.TestName) ---" | ||
|
|
||
| $testLogFile = Join-Path $OutputPath "test-with-fix-$($testEntry.TestName).log" |
There was a problem hiding this comment.
Same file-name sanitization issue for test-with-fix-<name>.log: $testEntry.TestName can contain characters invalid for file names (notably | from OR filters). Use a sanitized identifier for file paths and keep the original name only for display/reporting.
| $testLogFile = Join-Path $OutputPath "test-with-fix-$($testEntry.TestName).log" | |
| $sanitizedTestName = $testEntry.TestName -replace '[\\/:*?"<>|]', '_' | |
| $testLogFile = Join-Path $OutputPath "test-with-fix-$sanitizedTestName.log" |
| if ($PRNumber) { | ||
| # Get per-file patch from GitHub API | ||
| $patch = gh api "repos/dotnet/maui/pulls/$PRNumber/files" --jq ".[] | select(.filename == `"$file`") | .patch" 2>$null | ||
| } else { |
There was a problem hiding this comment.
For PR-based analysis, the device-test method extraction calls gh api .../pulls/$PRNumber/files inside the per-file loop. That refetches the full PR file list for every file and can be very slow (and hit rate limits) on large PRs. Fetch the PR files/patches once, cache them in memory (e.g., a filename→patch map), and then look up patches per file.
The gate phase (verify-tests-fail-without-fix) was hardcoded to only support UI tests. This extends it to auto-detect and run all test types.
Changes
Test Types Supported
Verified
Gate passed on local Share device test: without fix=FAIL ✅, with fix=PASS ✅