AOT Support for Spectre.Console#1690
Conversation
…ty with NetStandard 2.0 and AOT
…incompatability with AOT
… better in AOT scenarios
…iate for AOT scenarios. Additionally adds a warning to CommandApp for users who may try it.
|
This is already a great start and should already help in alot of use cases. What im mostly surprised about is that Spectre.Console is apparently easier to get Native AOT on than Spectre.Console.Cli (im assuming thats due to reflection in the background?) Considering most users of Spectre.Console also use the cli for their projects i would consider that the next big target. Maybe using source generators instead of reflection (again, if thats the case) |
When this is merged i will try it out on my projects that i was getting errors on and i will come back with my results here |
|
Very excited to try this out! |
|
Very excited to merge this! 😁 |
I agree with this general approach. I tried really hard to get Spectre.Console.Cli to work with a source generator, but ran into too many problems that directly affected the surface area. The dependency injection approach is already really hard to make work without reflection. But even stuff like this: app.Configure(config =>
{
config.AddCommand<AddCommand>("add");
config.AddCommand<CommitCommand>("commit");
config.AddCommand<RebaseCommand>("rebase");
})The By the time I got something kind-of working the API was just too different. It felt like a reboot. And at that point I figured I might as well try to reuse some code. My eventual solution was to build a "CLI" format for my serde system: https://github.com/dn-vm/dnvm/tree/main/src/Serde.CmdLine. Serde is a framework for expressing serializers and deserializers and uses a source generator. The model is intended to allow swapping in different formats. CLI parsing is, in some sense, a serialization/deserialization format. So I built a format for that. It's still very simplistic, but after I round off the edges I might create a NuGet package for it. |
This would be insanely cool if it would be able to work, however i think there would be some kind of way to do the thing in your example using for example roslyn source gens by grabbing the attributes of the Command specifying the options and settings and then the roslyn source gen would have to add code to the build manually setting the different options to their The roslyn source gen would have to generate some kind of line and add it to the source like: `Port = args.ParseThePortAndDoTheThings("--port ") So i dont think we would need a external lib for this, using some well defined roslyn generators these commands and settings can be set using some kind of functions injected at build time that contain all of the different properties and their respective cli strings |
|
I think at the end of the day to do AOT in Spectre.Console.Cli would either take considerable effort or a change in the API. Between DI, TypeConverters, MultiMap<>, dynamically creating arrays and even some magic in there for automatically instantiating things like Additionally, a while back, we split the Cli and Console app with the knowledge there were more "modern" libraries like System.CommandLine that integrate well with Spectre.Console that having the benefit of being developed after the introduction of things like assembly trimming have support designed in. I do think a third party that wanted to generate a source generator that scans for Settings and dumps a ton of |
Thats fair, so it seems this one is just gonna be: who wants to either put an unrealistic amount of effort to change the current Codebase or just rewrite it from scratch to native aot, so the options arent great here i guess. |
|
Once AOT for Spectre.Console is in, I will take a look at generating the command tree using source generators instead of doing it by reflection. I have some ideas. |
|
Redid the While I was in there I went ahead and added support for Spectre.Console.Json and Spectre.Console.ImageSharp, and I even added a blurb in the documentation's best practice section. |
373b838 to
4b2b284
Compare
|
FYI I’m totally willing to help anyone who wants to dive in the rabbit hole of writing a source generator. I just hit a wall and happened to have an alternative that I could use to unblock my app. FYI I’ve been using the rest of spectre console in dnvm as aot and it has worked great. |
…andling for stack frames.
4b2b284 to
f465619
Compare
|
I have nothing really to add here anymore. Pending a review, I say ready for merge. |
|
@phil-scott-78 I will be taking a look now. Not sure I will understand everything though 😁 |
|
Anything needing clarification or more comments to describe what's happening, I'm happy to expand! |
patriksvensson
left a comment
There was a problem hiding this comment.
It looks good to me. A question, though: Can we remove reliance on the other polyfill packages now that PolySharp has been introduced?
patriksvensson
left a comment
There was a problem hiding this comment.
It looks good to me. A question, though: Can we remove reliance on the other polyfill packages now that PolySharp has been introduced?
Add TrimmerRootAssembly for Spectre.Console.Cli and DeviceRunners.Cli to preserve types discovered via reflection (command settings classes). Spectre.Console.Cli is explicitly not trim-safe (spectreconsole/spectre.console#1690) so the entire assembly must be rooted. Suppress trim analysis warnings since Spectre.Console.Cli generates unavoidable IL2026/IL2090 warnings that are covered by the rooting. Result: 19MB self-contained single-file binary (down from 21MB multi-file). dotnet publish -r osx-arm64 --self-contained -p:PublishSingleFile=true Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add TrimmerRootAssembly for Spectre.Console.Cli and DeviceRunners.Cli to preserve types discovered via reflection (command settings classes). Spectre.Console.Cli is explicitly not trim-safe (spectreconsole/spectre.console#1690) so the entire assembly must be rooted. Suppress trim analysis warnings since Spectre.Console.Cli generates unavoidable IL2026/IL2090 warnings that are covered by the rooting. Result: 19MB self-contained single-file binary (down from 21MB multi-file). dotnet publish -r osx-arm64 --self-contained -p:PublishSingleFile=true Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ntegration (#110) * Add dotnet test support for Android and iOS via DeviceRunners.Testing.Targets Introduces a new MSBuild targets NuGet package that makes dotnet test work for Android and iOS MAUI test apps with a single command: dotnet test MyTests.csproj -f net10.0-android New: DeviceRunners.Testing.Targets NuGet package - build/DeviceRunners.Testing.Targets.props injects DEVICE_RUNNERS_AUTORUN, DEVICE_RUNNERS_PORT, DEVICE_RUNNERS_HOST_NAMES into the app bundle at build time via AndroidEnvironmentVariables / iOSEnvironmentVariables MSBuild items - build/DeviceRunners.Testing.Targets.targets overrides the VSTest target for Android/iOS to invoke the bundled CLI tool; selects the correct self-contained binary for the host RID (win-x64, linux-x64, osx-x64, osx-arm64) - PublishCliTools MSBuild target publishes the CLI self-contained + trimmed for all 4 RIDs before packing the NuGet package New: UseTestRunnerEnvironment() extension method Replaces the compile-time #if MODE_NON_INTERACTIVE_VISUAL block with a runtime env var check. When DEVICE_RUNNERS_AUTORUN is set (injected by the .targets file), the app auto-enables headless mode with TCP result streaming. Updated: sample test project - Removes #if MODE_NON_INTERACTIVE_VISUAL and TestingMode/MODE_NON_INTERACTIVE_VISUAL - Calls UseTestRunnerEnvironment() instead (no-op when env var is absent) - Imports DeviceRunners.Testing.Targets .props and .targets directly for in-repo use Updated: CI workflows (test-tcp-android-linux, test-tcp-ios) - Replaces 2-step dotnet publish + device-runners CLI with a single dotnet test call - Adds PublishCliTools step to build bundled native binaries before dotnet test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use dotnet pack instead of dotnet msbuild in CI to publish CLI binaries dotnet pack triggers PublishCliTools via BeforeTargets=GenerateNuspec, which is the natural way to prepare the NuGet package and its bundled binaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Android env var injection and iOS launch env vars for dotnet test support - Replace non-existent AndroidEnvironmentVariables ItemGroup with correct _GeneratedAndroidEnvironment items in a BeforeTargets=_GenerateEnvironmentFiles target. The Android SDK compiles these into the native libxamarin-app.so so that Mono makes them available via Environment.GetEnvironmentVariable() at app startup. - Use %3B to escape semicolons in HOST_NAMES value, preventing MSBuild from treating the list separator as an item separator. - Update iOS LaunchAppAsync to accept environmentVariables parameter and inject them using the xcrun simctl SIMCTL_CHILD_* mechanism when provided. - Update iOS TestCommand to pass DEVICE_RUNNERS_* env vars at app launch time (matching how Windows and macOS CLI commands already inject them). - Add DeviceRunners.Cli FDD publish output to tools/ for in-repo testing. Android dotnet test now works end-to-end: 44 tests, 36 pass, 5 intentional fail, TRX written, correct non-zero exit code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix review findings and .app bundle glob for macOS/iOS dotnet test - Fix inaccurate comment in MacOSService: 'open --env' does support env vars; direct launch is used for process handle retention and simplicity - Read stderr on xcrun simctl launch failure for better CI diagnostics - Make iOS CI dotnet pack use 'release' consistently with macOS CI - Fix .app bundle discovery: MSBuild Include glob (*.app/) does not reliably match directories on macOS; use Directory.GetDirectories instead — verified working for both iOS and macOS Catalyst - Remove committed binary DLLs from tools/ and add .gitignore so they are regenerated by dotnet pack (PublishCliTools target) - Validated end-to-end: dotnet test works for macOS Catalyst (44 tests, TRX generated) and iOS simulator (44 tests, TRX generated) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update AzDO pipelines to use dotnet test via DeviceRunners.Testing.Targets Mirror the same simplification already applied to GitHub Actions: - Replace 'dotnet tool install + dotnet publish + device-runners <plat> test' with 'dotnet pack Testing.Targets + dotnet test' - Remove TestingMode=NonInteractiveVisual (replaced by env var auto-config) - Remove manual .app/.apk/.exe discovery steps - All four platforms updated: Android, iOS, macOS, Windows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add global.json pinned to .NET 10.0.x and skip Xcode version check - global.json: pin SDK to 10.0.100 with rollForward=latestFeature so any 10.0.* SDK satisfies the requirement. - Directory.Build.props: set ValidateXcodeVersion=false so minor Xcode version mismatches don't break builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve dotnet test output: replace MSB3073 with human-readable summary Instead of the raw MSB3073 'command exited with code 1' error: - All Exec tasks now use IgnoreExitCode=true and capture the exit code - New _DeviceRunnersReportResults target parses the TRX via XmlPeek and emits a dotnet-test-style summary line, e.g.: Failed! - Failed: 5, Passed: 36, Skipped: 3, Total: 44 - On failure, emits a clean Error task message instead of MSB3073 - Fallback error for the case where CLI crashes before writing TRX - Fix: count skipped via UnitTestResult[@outcome='NotExecuted'] elements because xunit/nunit skipped tests don't increment Counters/@notExecuted Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restructure MSBuild targets: DependsOnTargets chain, shared args, single Exec Replace CallTarget with proper DependsOnTargets chains and eliminate duplicate Exec blocks. New target hierarchy: VSTest → _DeviceRunnersCheckCli (fail fast before building) → Build → _DeviceRunnersRunTests → _DeviceRunnersPrepareArgs → _DeviceRunnersCommonArgs (TRX path, device flag, shared CLI flags) → _DeviceRunnersAndroidArgs (Condition=android) → _DeviceRunnersiOSArgs (Condition=ios) → _DeviceRunnersMacOSArgs (Condition=maccatalyst) → _DeviceRunnersWindowsArgs (Condition=windows) → _DeviceRunnersExecTests (single Exec, consumes $(_DeviceRunnersCliArgs)) → _DeviceRunnersReportResults (XmlPeek TRX, emit summary) Each platform arg target sets $(_DeviceRunnersCliArgs); the optional $(_DeviceRunnersDeviceArg) is built once in _DeviceRunnersCommonArgs and appended where the CLI supports it (Android, iOS). No duplicate Exec pairs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: restructure MSBuild targets with DependsOnTargets chains - Replace CallTarget usage with proper DependsOnTargets chains - VSTest -> _DeviceRunnersCheckCli;Build;_DeviceRunnersRunTests - _DeviceRunnersRunTests -> _DeviceRunnersPrepareArgs;_DeviceRunnersExecTests;_DeviceRunnersReportResults - _DeviceRunnersPrepareArgs -> _DeviceRunnersCommonArgs + 4 platform targets - Introduce _DeviceRunnersPlatform and _DeviceRunnersIsSupportedPlatform properties so all conditions use property comparisons instead of repeated function calls - Single _DeviceRunnersExecTests consumes fully-assembled $(_DeviceRunnersCliArgs) - Single _DeviceRunnersDeviceArg with leading space for clean concatenation - ASCII-only comments (no box-drawing chars, no -- inside XML comments) Validated: dotnet test macOS Catalyst - 44 tests (36 pass, 5 fail, 3 skip) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: match dotnet test output format for results summary Emit the standard dotnet test two-line format: Results File: /path/to/test-results.trx Test summary: total: 44, failed: 5, succeeded: 36, skipped: 3, duration: 9.3s Changes: - Output 'Results File:' line first (before summary, not after) - Use 'succeeded' instead of 'passed' to match dotnet test terminology - Read scalar TRX counters as PropertyName (not ItemName) for clean interpolation - Compute duration from TRX Times/@start and Times/@finish attributes - Keep bare 'Test run failed.' Error as the MSBuild failure signal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use TESTERROR error code to match dotnet test output Adds Code="TESTERROR" to the two test-failure Error tasks so the output matches what dotnet test emits for failing tests: error TESTERROR: Test run failed. Tools that parse dotnet test output keyed on the TESTERROR code will now correctly recognise device test failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: embed test summary in TESTERROR for terminal logger visibility The dotnet CLI terminal logger (default in .NET 8+) filters MSBuild Message tasks, so Results File and Test summary were invisible at default verbosity. Fix by embedding the full summary in the TESTERROR error message which is always surfaced: error TESTERROR: Test run failed: 5 of 44 tests failed (36 succeeded, 3 skipped, duration: 9.7s). Results File: /path/... The Message tasks are kept for --tl:off / --verbosity normal users where they render as the clean two-line dotnet test format: Results File: /path/to/test-results.trx Test summary: total: 44, failed: 5, succeeded: 36, skipped: 3, duration: 9.7s Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: emit Results File and Test summary as separate TESTERROR lines Use %0A (MSBuild newline escape) to split the error into two lines matching the standard dotnet test output format: error TESTERROR: Results File: /path/to/test-results.trx error TESTERROR: Test summary: total: 44, failed: 5, succeeded: 36, skipped: 3, duration: 18.5s Both lines are always visible via the terminal logger. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: drop Results File from TESTERROR, keep Test summary only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: preserve test assemblies from iOS/Android linker Test classes are discovered via reflection at runtime. Without explicit trimmer roots the iOS/Android linker strips the referenced test assemblies (MauiLibrary.XunitTests, Library.NUnitTests), causing only tests in the main DeviceTests assembly to run (~8 instead of ~44). TrimmerRootAssembly tells the linker to keep the full assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: detect app crash during test run via missing end event The event stream protocol sends begin/end events to bracket the test run. If the TCP connection closes after receiving results but without the end event, the app crashed or was killed mid-run. Previously this produced partial results with no indication of a crash. Now the CLI explicitly reports: The application appears to have crashed during the test run. Only 8 test result(s) were received before the connection was lost. Check the device log for crash details. And forces a non-zero exit code even if all received tests passed, so CI pipelines correctly fail. Changes: - EventStreamService: add HasStarted and HasEnded tracking - BaseTestCommand: check for begin-without-end after listener completes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: use CLI exit code 2 for app crash detection CLI exit codes: 0 = all tests passed 1 = test failures 2 = app crashed (begin event received, results streamed, but no end event) MSBuild targets check the exit code directly (no file reading needed): exit code 2 + TRX: 'Test summary: ... (incomplete: app crashed)' exit code 2 + no TRX: 'The application crashed before producing any test results' exit code 1 + TRX: 'Test summary: ...' (normal failure) Validated: iOS: 8 tests, crash detected, '(incomplete: app crashed)' shown macOS: 44 tests, no crash, normal TESTERROR shown Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: replace tuple return with TestListenerResult record Replace the (int testFailures, string? testResults) tuple with a protected record TestListenerResult(int FailedCount, string? ResultsFile, bool Crashed). This makes the crash detection explicit via a bool rather than a sentinel value, and gives clear property names at all call sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: add ToExitCode() and Success to TestListenerResult Centralise exit code mapping and success logic on the record itself: - ToExitCode(): 0 = success, 1 = test failures, 2 = app crashed - Success: true when no failures and no crash All callers now use listener.ToExitCode() and listener.Success instead of duplicating the conditional logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: bump MAUI to 10.0.60 to fix Button.LayoutButton NRE on iOS MAUI 10.0.20 had a NullReferenceException in Button.LayoutButton when the platform button was null during a layout pass on the UI thread. This crashed the app mid-test-run on iOS, producing only partial results. The fix (dotnet/maui#35284, fixing dotnet/maui#31048) adds a null guard in LayoutButton. MAUI 10.0.60 includes this fix. Results: iOS now completes all 44 tests (previously crashed at 8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: filter iOS device log to app process for managed exception details Use the existing AppleDev predicate parameter on GetLogsPlainAsync to filter logs to just the app process: predicate: 'process == "DeviceTestingKitApp.DeviceTests"' Results: Before: 17,000 lines of OS-level noise, no managed exceptions After: 190 lines with all PASS/FAIL output and full managed stack traces when the app crashes (NullReferenceException, etc.) Also restores MAUI 10.0.60 (was temporarily reverted for testing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: bump AppleDev 0.8.7->0.8.10, AndroidSdk 0.34.0->0.35.1 AppleDev 0.8.10 adds SimCtl.LaunchAppAsync overload with env vars, so we can replace the manual ProcessStartInfo + SIMCTL_CHILD_* workaround with the native API call. AndroidSdk 0.35.1 is the latest stable release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add WindowsPackageType=MSIX to packaged Windows CI workflows The Testing.Targets .props defaults WindowsPackageType=None (for dotnet test unpackaged mode). The TCP Windows packaged and XHarness Windows workflows need MSIX packaging, so pass -p:WindowsPackageType=MSIX explicitly. Fixes: TCP Windows (GH+AzDO), XHarness Windows (GH+AzDO) CI failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add dotnet test documentation as primary testing approach New articles: - using-dotnet-test.md: Main guide covering setup, configuration, output format, crash detection, MSBuild properties, and how it works - dotnet-test-android.md: Emulator setup, env var injection, APK config - dotnet-test-ios.md: Simulator setup, device logs, crash troubleshooting - dotnet-test-macos.md: Direct launch, no simulator needed - dotnet-test-windows.md: Unpackaged EXE mode, WindowsPackageType Updated: - toc.yml: 'dotnet test (Recommended)' section at top, CLI renamed to 'Advanced', XHarness renamed to 'Legacy' (from 'CLI Runners') - index.md: Getting Started leads with dotnet test - README.md: dotnet test as primary, links to docs site - technical-architecture-overview.md: Testing.Targets package section with MSBuild target chain, exit code protocol, package structure - ci-pipeline.md: dotnet test CI examples for GH Actions and AzDO Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: detect Windows package type instead of overriding it Remove WindowsPackageType=None default from .props entirely. Instead, the _DeviceRunnersWindowsArgs target detects what the build produced: - If .exe exists (unpackaged): uses 'windows test --app path.exe' - If .msix exists (packaged): uses 'windows test --app path.msix' - If neither: clear error message This means dotnet publish for MSIX packaging is no longer broken by the Testing.Targets package. CI workflows no longer need -p:WindowsPackageType overrides. Also search both artifacts/ and sample/ for .msix in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: extract DeviceRunners.VisualRunners.Core for xplat types Create DeviceRunners.VisualRunners.Core (net9.0;net10.0 only) with the cross-platform testing types that the CLI needs: Moved from VisualRunners: - Testing/ITestResultInfo, ITestCaseInfo, ITestAssemblyInfo, ITestAssemblyConfiguration, TestResultStatus - Testing/Channels/IResultChannel, IResultChannelFormatter, FileResultChannel, FileResultChannelOptions, TestResultEvent, EventStreamFormatter, TextResultChannelFormatter, TrxResultChannelFormatter, TextWriterResultChannel The CLI now references VisualRunners.Core instead of VisualRunners, breaking the dependency on multi-target mobile TFMs. This unblocks self-contained and AOT publish (no more Mono runtime pack errors). VisualRunners references VisualRunners.Core transitively, so all existing consumers are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: enable trimmed single-file publish for CLI tool Add TrimmerRootAssembly for Spectre.Console.Cli and DeviceRunners.Cli to preserve types discovered via reflection (command settings classes). Spectre.Console.Cli is explicitly not trim-safe (spectreconsole/spectre.console#1690) so the entire assembly must be rooted. Suppress trim analysis warnings since Spectre.Console.Cli generates unavoidable IL2026/IL2090 warnings that are covered by the rooting. Result: 19MB self-contained single-file binary (down from 21MB multi-file). dotnet publish -r osx-arm64 --self-contained -p:PublishSingleFile=true Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: default CLI to self-contained single-file trimmed publish CLI csproj now has SelfContained, PublishSingleFile, PublishTrimmed, and RuntimeIdentifiers (osx-arm64;win-x64) set by default. Plain 'dotnet publish -r osx-arm64' produces a 19MB single-file binary. The NuGet package PublishCliTools target overrides these back to framework-dependent (SelfContained=false, PublishSingleFile=false, PublishTrimmed=false) since the bundled tool runs via 'dotnet <dll>'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: bundle per-RID single-file CLI binaries in NuGet package The NuGet package now ships self-contained, trimmed, single-file native binaries for each platform in tools/<rid>/: tools/osx-arm64/DeviceRunners.Cli (~20 MB) tools/win-x64/DeviceRunners.Cli.exe (~20 MB) tools/linux-x64/DeviceRunners.Cli (~20 MB) The MSBuild targets detect the host OS and select the right binary. Falls back to framework-dependent DLL at tools/DeviceRunners.Cli.dll for in-repo dev or older packages. Changes: - CLI csproj: add JsonSerializerIsReflectionEnabledByDefault, linux-x64 RID - Testing.Targets csproj: PublishCliTools publishes 3 RIDs (self-contained) - .targets: detect host OS -> pick tools/<rid>/DeviceRunners.Cli[.exe] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove framework-dependent DLL fallback from targets Only the per-RID single-file native binary is used now. The fallback to tools/DeviceRunners.Cli.dll is removed since it will never exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: convert TCP Windows packaged workflow to dotnet test Replace manual CLI tool install + device-runners commands with a single dotnet test call, matching all other TCP workflows. Removes: - dotnet pack + dotnet tool install of CLI global tool - Manual cert install/uninstall via device-runners windows cert - Manual dotnet publish + MSIX search + device-runners windows test The Testing.Targets package handles build, deploy, and test collection. All 10 TCP workflows (5 GH Actions + 5 AzDO) now use dotnet test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add pack step to CI, use WindowsPackageType, clean up section headings CI fixes: - Add 'Publish CLI Tool Binaries' (dotnet pack) step to macOS, Windows, and Windows Unpackaged workflows (GH Actions + AzDO) so the native CLI binary is available before dotnet test runs. MSBuild targets: - Windows args: use $(WindowsPackageType) == 'None' to distinguish unpackaged (.exe) vs packaged (.msix) instead of probing file existence. - Clean up XML section headings from ugly '===...' padding to '=[ name ]=' - Remove auto-publish hack from _DeviceRunnersCheckCli Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove _DeviceRunnersCheckCli dev-only target The NuGet package always ships the native binary. No need for a validation target in shipping targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: gate Android env var injection on dotnet test only, remove stale comment - _DeviceRunnersAndroidEnvironment now requires DeviceRunnersInjectEnvironment=true which is only set by _DeviceRunnersPreBuild (runs before Build in the VSTest chain). A plain 'dotnet build' no longer bakes DEVICE_RUNNERS_AUTORUN=1 into the APK. - Remove stale reference to _DeviceRunnersCheckCli in VSTest comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add display names for AzDO Windows job templates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: detect host architecture for CLI RID, publish all 6 RIDs Targets: detect host CPU architecture via RuntimeInformation.OSArchitecture to pick the correct RID (osx-arm64 vs osx-x64, etc.). Fixes 'Bad CPU type in executable' on Intel macOS CI runners. NuGet package now ships 6 RIDs: osx-arm64, osx-x64, win-x64, win-arm64, linux-x64, linux-arm64 PublishCliTools uses /nodeReuse:false to prevent file locking between sequential publishes on Windows. TCP Windows workflow uses WindowsPackageType=None (unpackaged) since dotnet test calls Build, not Publish, and MSIX requires Publish. MSIX-from-build support tracked separately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: restore CLI workflow for Windows packaged MSIX tests dotnet test uses Build (not Publish) so it can't produce MSIX packages. Restore the direct CLI approach for the packaged Windows test workflow: dotnet pack CLI -> dotnet tool install -> dotnet publish app -> device-runners test The unpackaged Windows workflow continues to use dotnet test. MSIX-from-build support tracked separately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: always pack Testing.Targets with release configuration The CLI tool and targets package are tooling, not the app under test. Always use -c release regardless of the test app's configuration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: note MSIX not yet supported via dotnet test, simplify Windows targets - using-dotnet-test.md: comparison table shows MSIX as 'Coming soon' - dotnet-test-windows.md: MSIX section with note and CLI redirect - .targets: remove MSIX detection code, simplify to .exe only with clear error message mentioning WindowsPackageType=None Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: search project directory for MSIX AppPackages output MSIX packaging puts output in <project>/AppPackages/, not artifacts/. Search both the project directory and artifacts/ for all Windows packaged workflows (TCP and XHarness, GH Actions and AzDO). This was a pre-existing bug from main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: revert MSIX search to artifacts only (matches main) MSIX output lands at artifacts/bin/.../AppPackages/ which is already under artifacts/. The recursive search finds it there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: improve Windows .exe not found error message Make it clear that dotnet test only supports unpackaged mode and direct users to the CLI for MSIX. We never set WindowsPackageType in the targets - we only read what the user configured. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: search entire repo for MSIX output AppPackages output location varies depending on SDK version and UseArtifactsOutput configuration. Search from repo root instead of restricting to artifacts/ to handle all cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: filter MSIX search to AppPackages directories only The broad recursive search from repo root was picking up runtime dependency MSIX files (e.g. .WindowsAppRuntime.1.8.msix) instead of the app MSIX. Filter to only match files in AppPackages dirs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: pass WindowsPackageType=None in unpackaged Windows CI workflows The sample project doesn't set WindowsPackageType in its csproj, so it defaults to MSIX packaging. The unpackaged CI workflows need to pass -p:WindowsPackageType=None explicitly (matching what main does with dotnet publish). Without this, the app builds as a packaged stub that ignores ProcessStartInfo env vars, causing the TCP timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove SelfContained/PublishSingleFile from CLI csproj SelfContained=true in the csproj breaks dotnet pack/tool install (the global tool becomes self-contained which doesn't work). Move these flags to the PublishCliTools command line instead, where they only apply to the NuGet package binary publish. PublishTrimmed stays in the csproj (it only activates during publish). This fixes TCP Windows packaged: the device-runners global tool was being installed as self-contained, causing it to malfunction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: restore MODE_NON_INTERACTIVE_VISUAL for MSIX packaged Windows tests UseTestRunnerEnvironment() reads env vars at runtime, but MSIX packaged apps are launched via the package manager and don't receive env vars from ProcessStartInfo. The packaged Windows CI workflow passes -p:TestingMode=NonInteractiveVisual which defines MODE_NON_INTERACTIVE_VISUAL and hardcodes the auto-start + TCP config at build time. Both paths coexist: UseTestRunnerEnvironment() for dotnet test (unpackaged), MODE_NON_INTERACTIVE_VISUAL for the CLI MSIX workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add /nodeReuse:false to pack commands to prevent file locking Multi-RID CLI publish during dotnet pack causes CS2012 file locking errors on Windows CI runners. Adding /nodeReuse:false to the pack command in both GH Actions and AzDO pipelines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add browser WASM platform support to Core and ConsoleResultChannel - Add net9.0-browser/net10.0-browser TFMs to DeviceRunners.Core - Add Platforms/Browser/DefaultAppTerminator.cs for WASM - Update Platforms/All/DefaultAppTerminator.cs to exclude BROWSER - Add ConsoleResultChannel to VisualRunners.Core for headless/WASM scenarios - Add Platforms/Browser/ conditional compilation to Core .csproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add DeviceRunners.VisualRunners.WebAssembly project New project providing WASM test runner infrastructure: - IWasmTestRunnerPlugin: extensible interface for test framework plugins (Xunit v2, v3, NUnit) - WasmTestRunnerBuilder: fluent builder for configuring WASM test runs without MAUI dependency - WasmTestRunner: orchestrates test execution and result channel lifecycle - WasmTestRunResult: summary of test execution results - Targets net9.0-browser and net10.0-browser Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add CLI wasm test and serve commands - WasmWebServerService: Kestrel-based static file server with WASM MIME types (.wasm, .dll, .pdb, .dat, .blat, .webcil) and cross-origin headers - BrowserService: Playwright-based headless browser with console capture - WasmTestCommand: end-to-end WASM test execution (serve → browser → console capture → EventStreamService → results) - WasmServeCommand: utility to serve WASM apps for manual testing - Add Microsoft.Playwright and ASP.NET Core framework references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add FrameworkReference and fix Blazor component compilation - Add Microsoft.AspNetCore.App FrameworkReference to csproj (required for Razor Class Libraries targeting net9.0/net10.0) - Add component namespace imports to _Imports.razor so child components (HomePage, TestAssemblyPage, StatusBadge, etc.) resolve correctly - Fix TestAssemblyPage: cast TestCases to INotifyCollectionChanged before subscribing to CollectionChanged (IList<T> does not expose it) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add Xunit/NUnit WASM plugins and Blazor visual runner Xunit WASM plugin (DeviceRunners.VisualRunners.Xunit.WebAssembly): - XunitWasmTestRunnerPlugin: discovers and runs Xunit v2 tests in browser - WasmXunitExecutionSink: captures test results for IResultChannel - WasmTestRunnerBuilder.AddXunit() extension method NUnit WASM plugin (DeviceRunners.VisualRunners.NUnit.WebAssembly): - NUnitWasmTestRunnerPlugin: discovers and runs NUnit tests in browser - WasmNUnitTestListener: captures test results for IResultChannel - WasmTestRunnerBuilder.AddNUnit() extension method Blazor visual test runner (DeviceRunners.VisualRunners.Blazor): - Razor Class Library with Blazor components - TestRunnerApp, HomePage, TestAssemblyPage, TestResultPage, DiagnosticsPage - Reuses existing ViewModels from DeviceRunners.VisualRunners - BlazorVisualRunnerExtensions for DI registration - CSS styling for test status indicators Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add sample WASM test app and dotnet test integration Sample app (DeviceTestingKitApp.BrowserTests): - Targets net9.0-browser with Xunit tests - Uses WasmTestRunnerBuilder with AddXunit() plugin - ConsoleResultChannel for NDJSON output to Console.Out - Demonstrates passing, theory, and skipped tests dotnet test integration (DeviceRunners.Testing.Targets): - Add 'browser' to supported platforms - _DeviceRunnersWasmArgs target: publishes WASM app, invokes CLI wasm test - Configurable timeout (DeviceRunnersWasmTimeout) and browser (DeviceRunnersWasmBrowser) MSBuild properties - Automatic wwwroot detection in publish output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: handle CurrentUser cert store as fallback for non-admin MSIX testing InstallCertificate, UninstallCertificate and IsCertificateInstalled now try/check LocalMachine\TrustedPeople first and fall back to CurrentUser\TrustedPeople. This allows running 'device-runners windows test' without admin rights (the CLI no longer crashes); note that Add-AppxPackage itself still requires the cert to be in LocalMachine\TrustedPeople on most Windows configurations, so MSIX install will only succeed when the process has admin rights or the cert is pre-installed at the machine level. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: add WASM browser test jobs to GitHub Actions and Azure DevOps GitHub Actions: - Add wasm-browser job to ci.yml running on ubuntu-24.04 - Create test-wasm-browser composite action: publish → Playwright install → CLI wasm test - Install wasm-tools workload in setup-tools Azure DevOps: - Add WASM_Browser_Tests stage to azure-pipelines.yml - Create test-wasm-browser.yml template with same flow - Install wasm-tools workload in setup-tools.yml Both pipelines: - Publish WASM app → find wwwroot → run device-runners wasm test - Produce TRX results for test reporting - Upload logs and test results as artifacts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove duplicate Blazor project entry from solution file The solution file had DeviceRunners.VisualRunners.Blazor listed twice, causing MSB4025 'Duplicate item' error on CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use Blazor WebAssembly SDK for browser test sample - Switch from Microsoft.NET.Sdk to Microsoft.NET.Sdk.BlazorWebAssembly so publish produces wwwroot with index.html and .wasm files - Add Microsoft.AspNetCore.Components.WebAssembly package - Add wwwroot/index.html with Blazor bootstrap script - Update Program.cs to use WebAssemblyHostBuilder - Add net9.0/net10.0 TFMs to WebAssembly plugin projects for Blazor compat - Fix Playwright install step in CI (use playwright.ps1 from NuGet cache) - Improve WASM publish path detection in CI (AppBundle, .wasm fallbacks) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "fix: handle CurrentUser cert store as fallback for non-admin MSIX testing" This reverts commit 8504d57b1a2ca22ce58c0b05eea8903412d02956. * refactor: drop net9.0-browser TFMs, use plain net9.0 for WASM Blazor WebAssembly uses plain net9.0 with Microsoft.NET.Sdk.BlazorWebAssembly, not net9.0-browser. Remove the browser-specific TFMs and platform folders since they aren't needed for the Blazor-based approach. - Remove net9.0-browser/net10.0-browser from Core, WebAssembly, Xunit and NUnit projects - Remove Platforms/Browser/ folder and conditional compilation from Core - Remove wasm-tools workload from CI setup (not needed for Blazor WASM) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use PowerShell for Playwright browser installation in CI The bash-based playwright.ps1 invocation was failing with MethodInvocationException. Switch to native pwsh shell for both GitHub Actions and Azure DevOps templates. Pack failure (CS2012 file lock) is a pre-existing Windows CI flake, not related to this PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: replace Playwright with self-contained Chrome DevTools Protocol Drop the Microsoft.Playwright dependency entirely. Instead, launch the system-installed Chrome/Chromium directly and communicate via CDP over WebSocket. This is the same approach XHarness uses (Selenium + CDP) but without requiring any external tools to be installed. BrowserService now: - Finds Chrome/Chromium/Edge on the system (macOS, Linux, Windows) - Supports CHROME_PATH environment variable override - Launches headless with --remote-debugging-port=0 (auto-assign) - Connects to CDP WebSocket and subscribes to Runtime.consoleAPICalled - Navigates via Page.navigate CDP command - Captures all console.log output as NDJSON events Removed: - Microsoft.Playwright NuGet package - Playwright browser install steps from GitHub Actions and Azure DevOps - --browser CLI option (Chrome/Chromium only via CDP) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use reflection-based test runner for WASM instead of XunitFrontController XunitFrontController requires filesystem access to load assemblies by path, which is not available in browser WASM. Replace with a reflection-based approach that discovers [Fact] and [Theory] methods from already-loaded assemblies and executes them directly. - Find test methods via reflection on [FactAttribute] and [InlineDataAttribute] - Execute tests with proper async support and IDisposable cleanup - Handle skip reasons, TargetInvocationException unwrapping - Remove xunit.runner.utility dependency (only need xunit for attributes) - Remove WasmXunitExecutionSink (no longer using XunitFrontController) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: replace ASP.NET Core web server with HttpListener The FrameworkReference for Microsoft.AspNetCore.App combined with the existing RuntimeIdentifiers caused NU1102 errors on CI — NuGet tried to resolve ASP.NET Core runtime packs for all 6 RIDs (osx-arm64, win-x64, etc). Replace the Kestrel-based WasmWebServerService with a lightweight HttpListener implementation. Zero new dependencies — HttpListener is built into .NET. The CLI csproj now has zero diff from the base branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: rename Windows CI jobs to (MSIX) and (EXE) matching main Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add Windows MSIX support via loose deploy for dotnet test Now that the CLI supports loose-file MSIX registration (via winapp.exe from #94), dotnet test works for both Windows modes: - Unpackaged (WindowsPackageType=None): detects .exe in build output - Packaged (default MSIX): detects AppxManifest.xml in build output, CLI registers the app via winapp.exe loose deploy TCP Windows (MSIX) CI workflows converted from manual CLI to dotnet test. No more cert management, dotnet publish, or MSIX file search needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add developer mode and WinAppSDK runtime to Windows MSIX dotnet test Loose-file MSIX registration requires Windows Developer Mode enabled and the Windows App Runtime framework installed. These steps were in the loose workflow but missing from the converted dotnet test workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: separate CLI and dotnet test CI jobs Restore test-tcp-* workflows to main's CLI-direct versions so the CLI path continues to be tested. Add new test-dotnet-test-* workflows that test the dotnet test integration via Testing.Targets. New CI jobs (5 GH Actions + 5 AzDO): - dotnet test Android (Linux) - dotnet test iOS (x64) - dotnet test macOS (x64) - dotnet test Windows (MSIX) - loose deploy - dotnet test Windows (EXE) - unpackaged Existing CLI jobs restored from main: - TCP Android, iOS, macOS, Windows (MSIX), Windows (EXE), Windows (Loose) Both paths are now tested independently in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: rename UseTestRunnerEnvironment to AddEnvironmentVariables PR #111 landed the method as AddEnvironmentVariables, not UseTestRunnerEnvironment. Fix MauiProgram.cs and docs to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: restore RootMode=all on TrimmerRootAssembly entries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use PublishForTargetsPackage property for CLI publish mode Instead of passing --self-contained and -p:PublishSingleFile=true on the command line, the CLI csproj uses a PublishForTargetsPackage property to gate SelfContained, PublishSingleFile, and PublishTrimmed. The project decides what's best for each publish mode: -p:PublishForTargetsPackage=true -> self-contained, single-file, trimmed (default) -> framework-dependent tool Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address review findings (CLI args, Android env, .app lookup, docs, TRX) 1. Rename AddEnvironmentVariables -> AddCliConfiguration: reads env vars first, then falls back to CLI args (--device-runners-autorun etc.). This enables MSIX packaged Windows apps where env vars can't be forwarded but winapp.exe --args can pass CLI arguments. AddEnvironmentVariables kept as deprecated alias. 2. Windows loose launch now passes --device-runners-* CLI args via winapp.exe --args so MSIX apps auto-start without MODE_NON_INTERACTIVE. 3. Android env var injection: always inject when Testing.Targets is referenced (remove DeviceRunnersInjectEnvironment gate). Fixes stale APK issue with incremental builds. 4. iOS/macOS .app lookup: use $(AssemblyName).app instead of *.app to avoid matching multiple .app directories in the output. 5. Remove _DeviceRunnersPreBuild target (no longer needed). 6. Fix stale docs: remove _DeviceRunnersCheckCli references, update method names, fix package layout description. 7. Add TRX test-reporter to all dotnet test GH Actions jobs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: unify WASM with MAUI visual runner pattern Delete redundant WebAssembly projects that duplicated existing abstractions: - Remove DeviceRunners.VisualRunners.WebAssembly (IWasmTestRunnerPlugin, WasmTestRunnerBuilder) - Remove DeviceRunners.VisualRunners.Xunit.WebAssembly - Remove DeviceRunners.VisualRunners.NUnit.WebAssembly Instead, add WASM-compatible implementations of the standard interfaces directly in DeviceRunners.VisualRunners.Xunit: - XunitWasmTestDiscoverer (ITestDiscoverer) — reflection-based discovery - XunitWasmTestRunner (ITestRunner) — reflection-based execution - AddXunitWasm() extension on IVisualTestRunnerConfigurationBuilder The sample app now mirrors the MAUI pattern exactly: builder.Services.AddBlazorVisualTestRunner(conf => { conf.AddTestPlatform<XunitWasmTestDiscoverer, XunitWasmTestRunner>(); conf.AddTestAssembly(typeof(Tests).Assembly); conf.EnableAutoStart(autoTerminate: true); conf.AddResultChannel(_ => new ConsoleResultChannel(new EventStreamFormatter())); }); builder.RootComponents.Add<TestRunnerApp>("#app"); Same binary works both interactively (visual UI) and headlessly (CLI). Also: - Fix Blazor csproj: use PackageReference for ASP.NET Components instead of FrameworkReference (no browser-wasm runtime pack for ASP.NET Core) - Fix AddBlazorVisualTestRunner to accept IVisualTestRunnerConfigurationBuilder - Merge mattleibow/investigate-dotnet-test-mobile (3 commits) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add dotnet restore before WinAppSDK install in Windows MSIX workflows The WinAppSDK runtime install step needs the NuGet package cache to be populated. Add an explicit dotnet restore of the test project before the install step to ensure microsoft.windowsappsdk is in the cache. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * revert: restore MAUI 10.0.20 to match main The MAUI 10.0.60 bump may have changed MSIX output paths, breaking the TCP Windows (MSIX) CI workflow. Revert to main's version (10.0.20). MAUI version bump can be done in a separate PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use xunit SDK internals for proper WASM test discovery and execution Replace naive reflection-based [Fact]/[InlineData] scanning with xunit's own discovery and execution engine, adapted for single-threaded WASM: - WasmXunitDiscoverer: extends XunitTestFrameworkDiscoverer with threadless discovery via SynchronousMessageBus (handles Theory, MemberData, ClassData, all data sources) - WasmXunitAssemblyRunner/CollectionRunner/ClassRunner: yield-based execution chain for cooperative multitasking in single-threaded WASM - WasmExecutionSink: proper result collection via xunit message events - Updated XunitWasmTestCaseInfo to wrap ITestCase instead of raw reflection types, enabling full xunit feature support - Updated XunitWasmTestResultInfo to use ITestResultMessage with proper error/stack trace combining via ExceptionUtility - Added comprehensive sample tests (MemberData, async, IDisposable, ITestOutputHelper) verifying all features work end-to-end - Clean Program.cs using AddXunitWasm() extension Based on XHarness ThreadlessXunitTestRunner and YieldingXunit2 patterns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add BuildInParallel=false to pack to prevent file locking The multi-RID CLI publish during dotnet pack causes CS2012 file locking when parallel TFM builds (net9.0 + net10.0) compete for the same output files. /nodeReuse:false alone is insufficient because the outer pack spawns MSBuild nodes that the inner publish commands conflict with. BuildInParallel=false serializes the TFM builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: publish only host RID in CI dotnet test workflows Replace 'dotnet pack Testing.Targets' (which publishes all 6 RIDs) with a single 'dotnet publish CLI -r <host-rid>' in each workflow. Publishing 6 RIDs on a Linux runner takes 30+ minutes (cross-compiling Windows runtime packs) and was causing the Android job to time out. Each workflow now publishes only what it needs: - Android/Linux: linux-x64 - iOS/macOS: osx-x64 - Windows: win-x64 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: consolidate xunit types and unify Blazor/MAUI API Consolidation: - Delete duplicated Wasm wrapper types (XunitWasmTestCaseInfo, etc.) and reuse the existing XunitTestCaseInfo, XunitTestResultInfo, DeviceExecutionSink, XunitTestAssemblyInfo from the parent - Rename Wasm/ → Reflection/ (names what it IS, not where it's used) - XunitReflectionTestDiscoverer/Runner: threadless, reflection-based xunit discovery and execution, usable on any platform - AddXunit(useReflection: true) parameter instead of separate method API unification: - BlazorVisualTestRunnerConfigurationBuilder → VisualTestRunnerConfigurationBuilder - AddBlazorVisualTestRunner → UseVisualTestRunner (mirrors MAUI API) - Extension target: WebAssemblyHostBuilder (like MauiAppBuilder) Tests: - Add unit tests for reflection-based discoverer and runner Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove AddEnvironmentVariables backward compat alias Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: disable shared Roslyn compilation during pack to prevent file locking The VBCSCompiler server process holds DLLs open between sequential dotnet publish commands within the PublishCliTools target. Adding UseSharedCompilation=false forces each compilation to use its own process instead of the shared server, preventing file locking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add UseSharedCompilation=false to inner publish commands too The outer dotnet pack properties don't propagate to inner Exec'd dotnet publish commands since they spawn separate processes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: consolidate result channels and clean up xunit utilities Result channels: - Move ConsoleResultChannel and DebugResultChannel to VisualRunners.Core - Default to TextResultChannelFormatter (human-readable), accept optional formatter parameter for NDJSON or other formats - Delete duplicate internal copies from VisualRunners Xunit reflection utilities: - Delete NullMessageSink — replace with ConsoleDiagnosticMessageSink that routes xunit diagnostic messages to IDiagnosticsManager - Rename NullSourceInformationProvider → EmptySourceInformationProvider (not null — it returns empty source info, which is correct when PDBs aren't available) - Rename ReflectionXunitDiscoverer → XunitReflectionDiscoverer (consistent Xunit prefix) - Consolidate utilities into XunitReflectionUtilities.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove RuntimeIdentifiers from CLI csproj to prevent cross-RID locking RuntimeIdentifiers (plural) causes dotnet publish -r <rid> to evaluate ALL listed RIDs during restore/build, not just the requested one. This means sequential publishes for different RIDs conflict because the linker from the previous RID still holds files open when the next RID tries to re-evaluate the same paths. Each dotnet publish -r <rid> in PublishCliTools already specifies the RID explicitly, so RuntimeIdentifiers in the csproj is unnecessary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: split XunitReflectionUtilities into separate files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove RuntimeIdentifiers, simplify sequential per-RID publish RuntimeIdentifiers (plural) in the csproj caused each dotnet publish -r to evaluate ALL RIDs, leading to cross-RID file locking. Without it, each -r flag builds only the requested RID. Simplified PublishCliTools to inline the args per Exec (no shared property). Removed BuildInParallel/UseSharedCompilation workarounds from ci.yml since they were masking the real issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: unify DiagnosticMessageSink — delete ConsoleDiagnosticMessageSink Add IMessageSink implementation and IDiagnosticsManager constructor to the existing DiagnosticMessageSink instead of maintaining a separate class. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: simplify DiagnosticMessageSink to single IDiagnosticsManager constructor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct double-encoded XML entities in Exec commands Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: flatten Reflection/ folder and rename Yielding* → XunitYielding* - Move all files from Reflection/ to project root — no need for subfolder - YieldingXunitAssemblyRunner → XunitYieldingAssemblyRunner - YieldingXunitCollectionRunner → XunitYieldingCollectionRunner - YieldingXunitClassRunner → XunitYieldingClassRunner Consistent Xunit prefix across all types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use item batching for per-RID CLI publish Define RIDs as an ItemGroup and use %(_CliTargetRid.Identity) in a single Exec element. MSBuild batches the Exec once per item, running each RID sequentially. Cleaner than 6 copy-pasted Exec commands. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove nodeReuse workaround, revert XHarness changes to main nodeReuse:false was a workaround for RuntimeIdentifiers cross-RID evaluation which no longer applies. Reverted XHarness Windows workflows to match main exactly (display name and MSIX search path). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: fix stale and incorrect documentation across all .md files Key fixes: - using-dotnet-test.md: AddCliConfiguration reads env vars AND CLI args (not just env vars), updated config injection table for Windows MSIX, removed contradictory notes about MSIX not being supported, updated How It Works to mention loose MSIX layout - dotnet-test-macos.md: fixed glob pattern (.app not *.app) - dotnet-test-windows.md: detection table now shows AppxManifest.xml (loose MSIX layout) instead of .msix, clarified CLI args for MSIX - dotnet-test-ios.md: completed truncated Common causes section - ci-pipeline.md: added all 10 test-dotnet-test-* workflows to file reference tables, added dotnet test column to platform matrix, fixed incorrect claim that TCP workflows use dotnet test, fixed AzDO example line continuation (backtick -> backslash) - technical-architecture-overview.md: updated package structure from framework-dependent DLL to per-RID self-contained binaries, fixed Windows support indicator, updated launch description Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: support AddCliConfiguration for WASM via URL query params The CLI navigates to ?device-runners-autorun=1 to trigger headless mode. The app reads the current URL via JSImport and parses the query string during builder configuration. When the param is absent (manual browser open), auto-start is not enabled and the interactive visual runner shows. - AddCliConfiguration(string currentUrl) for WASM in Blazor extensions - CLI WasmTestCommand passes ?device-runners-autorun=1 in the URL - index.html exposes getLocationHref() JS function for JSImport - Sample Program.cs mirrors MAUI pattern: AddCliConfiguration + AddXunit - AllowUnsafeBlocks for JSImport support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: move BrowserInterop to Blazor library, auto-detect CLI in UseVisualTestRunner - Move JSImport BrowserInterop class from sample to DeviceRunners.VisualRunners.Blazor - UseVisualTestRunner automatically reads URL query params for CLI detection (no explicit AddCliConfiguration needed in the sample) - AddCliConfiguration(string url) overload for WASM URL query param parsing - Sample Program.cs is now clean — mirrors MAUI pattern exactly - AllowUnsafeBlocks moved from sample to Blazor library (for JSImport) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: clarify AddCliConfiguration works with both CLI and dotnet test Updated xmldoc, code comments, and docs to reflect that AddCliConfiguration reads config injected by either the DeviceRunners CLI (direct launch) or dotnet test (via MSBuild targets). Filed #123 for Android intent extras as a follow-up to replace build-time env var injection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: call out Android build-time vs launch-time config injection All platforms except Android inject configuration at launch time, allowing the same built app to be reused with different settings. Android embeds config into the APK at build time because adb has no env var mechanism. References #123 for the planned intent extras improvement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: move JS interop to library, align CI with existing patterns JS interop: - Move getLocationHref() JS to library wwwroot/device-runners-interop.js (shipped as RCL static asset at _content/.../) - index.html references library JS instead of inline script CI workflows (GitHub Actions + Azure DevOps): - Match existing platform pattern: dotnet pack → dotnet tool install --global - Use device-runners CLI as global tool (not dotnet run --project) - Simplified app path detection matching macOS/iOS/Windows patterns - Consistent step naming (Build and Install CLI Tool, Publish, Run Tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: include CLI tools in NuGet package via dynamic item inclusion The static <None Include="tools\**"> glob was evaluated at project load time, before PublishCliTools populated the tools/ directory. This meant a clean pack produced a nupkg with build/ but zero tools/ files. Fix: move the tools glob into the PublishCliTools target as a _PackageFiles item, so it runs AFTER the per-RID publishes complete. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: reference library CSS in sample index.html The CSS was shipped by the Blazor library but never referenced in the host page. Add link to _content/.../device-runners.css, proper viewport meta tag, and styled loading/error states. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add Library.XunitTests, fix Blazor UI click-to-run and search - Create DeviceTestingKitApp.Library.XunitTests: tests for shared library (CounterViewModel tests), mirroring MAUI's MauiLibrary.XunitTests - BrowserTests now references multiple test assemblies (like MAUI DeviceTests) - Fix: clicking a test now runs it first then navigates to result (matches MAUI) - Fix: search filter error handling for WASM sync context - Fix: CSS body margin/padding reset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: align Android dotnet test workflow with working TCP workflow The dotnet test Android workflow was missing several setup steps that the working TCP workflow has: - Java 21 installation (required by Android SDK/emulator) - mkdir for AVD directory (ANDROID_AVD_HOME must exist) - ANDROID_EMULATOR_HOME env var - AndroidSdkDirectory MSBuild property for build These omissions caused the emulator to not boot or the build to not find the Android SDK, resulting in the job hanging indefinitely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: align test coverage across MAUI and Blazor, add Credits page Tests: - Align BrowserTests with DeviceTests patterns (UnitTests, UnitTestsWithOutput, conditional failing, long-running async) - Add MemberData and IDisposable tests to DeviceTests (from BrowserTests) - Both now have equivalent test coverage - Add INCLUDE_FAILING_TESTS conditional compilation to BrowserTests Credits: - Add CreditsPage.razor to Blazor visual runner - Register CreditsViewModel in DI - Add Credits nav link to TestRunnerApp UI: - Fix scroll: nav pinned, main content scrolls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: scroll lists not pages, remove root back buttons, add ViewModel binding tests UI: - Assembly list, test list, diagnostics log, credits content scroll independently - Nav bar and page headers (Run All, search, filters) stay pinned - Remove back buttons from Home, Diagnostics, Credits (they're root nav tabs) Tests: - Add ViewModelBindingTests: CanLogin, PropertyChanged, ICommand, ClassData theory — Blazor equivalent of MAUI's TestPageUITests/DynamicUITests - Same tests added to both DeviceTests (MAUI) and BrowserTests (Blazor) - Text transform and ClassData tests mirror MAUI's DynamicUITests patterns Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: replace ClassData with MemberData to avoid serialization error ClassData with complex record types fails in xunit's reflection-based discoverer: 'Non-serializable data found'. Use MemberData with primitive parameters instead. Applied to both BrowserTests and DeviceTests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: support ClassData by setting PreEnumerateTheories=false The reflection-based discoverer was serializing theory data at discovery time (PreEnumerateTheories=true), which fails for non-serializable types like custom records. Setting false defers enumeration to execution time, matching XunitFrontController behavior. Restored ClassData tests alongside MemberData — both theory types now have coverage in both MAUI DeviceTests and Blazor BrowserTests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: create BlazorLibrary with CounterComponent and test parity New projects: - DeviceTestingKitApp.BlazorLibrary: Razor Class Library equivalent of MauiLibrary — CounterComponent.razor (Blazor equivalent of CounterView.xaml) and CounterValueFormatter (equivalent of CounterValueConverter) - DeviceTestingKitApp.BlazorLibrary.XunitTests: tests mirroring MauiLibrary.XunitTests — CounterValueFormatter tests for 0/1/N formatting plus synthetic pass/skip/fail Updates: - MauiLibrary.XunitTests: add REAL CounterValueConverter tests (was only synthetic Assert.True before) - BrowserTests: references BlazorLibrary.XunitTests assembly (3 assemblies now discovered) Test parity: | Test | MauiLibrary.XunitTests | BlazorLibrary.XunitTests | |------|----------------------|------------------------| | Convert/Format 0→Click me! | ✅ | ✅ | | Convert/Format 1→singular | ✅ | ✅ | | Convert/Format N→plural | ✅ | ✅ | | SuccessfulTest | ✅ | ✅ | | SkippedTest | ✅ | ✅ | | FailingTest (conditional) | ✅ | ✅ | Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: one class per file, delete Library.XunitTests - Split multi-class files into separate files matching MAUI convention - Delete DeviceTestingKitApp.Library.XunitTests — CounterViewModel tests moved to BlazorLibrary.XunitTests - BrowserTests now has 2 test assemblies (own + BlazorLibrary) matching MAUI's 2 (own + MauiLibrary) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: restore lost MauiLibrary test files, add Blazor equivalents Restored from base branch (lost during merges): - MauiLibrary.XunitTests/Controls/CounterViewTests.cs - MauiLibrary.XunitTests/Controls/VisualElementTests.cs - MauiLibrary.XunitTests/Converters/CounterValueConverterTests.cs - MauiLibrary.XunitTests/Features/MauiSemanticAnnouncerTests.cs Deleted duplicate CounterValueConverterTests.cs we incorrectly added. Added Blazor equivalents: - BlazorLibrary.XunitTests/Formatters/ (mirrors Converters/) - BlazorLibrary.XunitTests/Controls/ (CounterViewModelTests) - BlazorLibrary.XunitTests/Features/ (ConsoleSemanticAnnouncerTests mirrors MauiSemanticAnnouncerTests — tests the same ISemanticAnnouncer pattern with a delegating wrapper instead of MAUI's ISemanticScreenReader) Folder structure now mirrors MAUI: MAUI: Controls/ Converters/ Features/ UnitTests.cs Blazor: Controls/ Formatters/ Features/ UnitTests.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: exclude trim analysis warnings from TreatWarningsAsErrors PR #124 enabled TreatWarningsAsErrors globally. The CLI project has known trim warnings (IL2026, IL2090) from reflection-based JSON/XML serialization and Spectre.Console.Cli. These are suppressed during publish via SuppressTrimAnalysisWarnings but TreatWarningsAsErrors promoted them to errors during normal builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use JSON source gen and suppress XML trim warnings properly Replace reflection-based JsonSerializer calls with source-generated CliJsonContext for all CommandResult types and Dictionary<string,object>. This eliminates IL2026 warnings for JSON serialization and removes the need for JsonSerializerIsReflectionEnabledByDefault. XML serialization (XmlSerializer) has no source-gen equivalent, so those call sites use #pragma to suppress IL2026. Reflection-based text output (GetProperties) uses DynamicallyAccessedMembers annotations to satisfy IL2090. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: restore dropped comments, remove dead fallback code Restored comments that were accidentally removed during the source-gen refactor. Removed the dead JSON fallback path since all CommandResult types are registered in CliJsonContext. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: implement all test parity gaps from Opus review IAsyncLifetime + [Collection]: - Add AsyncLifetimeTests to both BrowserTests and DeviceTests - Exercises async setup/teardown and test serialization Component rendering: - Add CounterComponentTests using HtmlRenderer to render CounterComponent.razor and assert HTML output - Mirrors MAUI's CounterViewTests (initial state, command update) Production BlazorSemanticAnnouncer: - Add BlazorSemanticAnnouncer to BlazorLibrary (uses Action<string> for JS interop / console fallback) - Tests now target real production type, not inline stubs ComponentTestBase fixture: - Add ComponentTestBase with ctor/Dispose service provider pattern - Mirrors MAUI's VisualElementTests (verifies clean state) Hygiene: - Rename ComponentTests.cs → ViewModelBindingTests.cs - Restore LongRunningSuccess delay to 2000ms - Add negative/edge case to CounterValueFormatterTests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: move AsyncLifetimeTests out of UITests, use AsyncLifecycle collection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: remove static from ComponentTestBase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: suppress NU5128 for targets-only NuGet package DeviceRunners.Testing.Targets has IncludeBuildOutput=false because it ships only build/ and tools/ content (no lib/ assemblies). NU5128 warns about the mismatch, which TreatWarningsAsErrors promotes to an error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: close test parity gaps A, B, G, H Gap A — Button click event dispatch: - Add bunit to BlazorLibrary.XunitTests - CounterComponentClickTests dispatches onclick through the rendered component (mirrors MAUI SendClicked) Gap B — LoginComponent rendered tests: - Create LoginViewModel in BlazorLibrary (mirrors MAUI TestViewModel) - Create LoginComponent.razor (mirrors MAUI TestPage.xaml) - LoginComponentTests checks disabled attribute after VM property changes (mirrors MAUI TestPageUITests) - Remove duplicated LoginViewModel from BrowserTests; reference shared Gap G — MAUI null-guard: - Add ArgumentNullException guard to MauiSemanticAnnouncer ctor - Add NullScreenReaderThrows test Gap H — MAUI converter edge cases: - Add -1 and int.MaxValue rows to CounterValueConverterTests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review findings - Quote CLI tool path in Exec command to handle spaces in NuGet cache paths (e.g. user profiles with spaces) - Use AppPort/AppHostNames settings in Windows MSIX launch args instead of hardcoding Port and localhost, matching GetAppEnvironmentVariables - Remove TestingMode=NonInterac…
Made another run at AOT support. Spectre.Console itself mostly worked already, but with a few rough spots and unexpected behaviors for edge cases. The biggest issue may have been if someone was using prompts and relying on TypeConverters. @agocke did some work to use the intrinsic ones that I've included here.
ExceptionHelperis marked explicitly as not working, as is the entirety ofSpectre.Console.Cli.TypeConverterHelperbased on the work by @agocke to support trimming and AOTEnumUtilsfor better compatibility with NetStandard 2.0 and AOTRequiresDynamicCodeattribute to exception formatter to indicate incompatibility with AOT. Adds a fallback if someone tries to use it.TypeNameHelperto work better in AOT scenariosCommandAppfor users who may try it.Outstanding work would be
This replaces the previous PR #1508 and closes #1332 and #1401.
I feel this is pretty solid, and ready for review and merge. I know @Armando-CodeCafe, @BlazeFace, @antoinebj and @TheExiledCat all have express interest recently in the feature so their input would also be appreciated.
Please upvote 👍 this pull request if you are interested in it.