Skip to content

test: add Android and iOS device tests run through dotnet test --device - #4506

Merged
glennawatson merged 7 commits into
mainfrom
test/device-tests
Sep 26, 2026
Merged

glennawatson merged 7 commits into
mainfrom
test/device-tests

Conversation

@glennawatson

@glennawatson glennawatson commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ReactiveUI's Android and iOS platform code now has tests that run on a real emulator and simulator, in CI and locally.

  • ReactiveUI.Device.Tests replaces ReactiveUI.NonParallel.Mobile.Tests. The old project used xUnit and NUnit runners, referenced a project that no longer exists, and was never built.
  • The Android head runs through the .NET SDK's dotnet test --device. An instrumentation runs TUnit in the app and streams each result back, and dotnet test writes the TRX report on the host.
  • The iOS head runs every test when UIKit connects its scene. It writes a TRX report and an exit-code file, then exits with the test run's code.
  • Android tests cover HandlerSequencer, the WithAndroidX registrations, PlatformOperations, activity and fragment activation, StartActivityForResultAsync, ControlFetcherMixins on views, activities, layout hosts and AndroidX fragments, LayoutViewHost, ReactiveViewHost, AutoSuspendHelper, BundleSuspensionDriver, shared-preference changes, service binding, and the RecyclerView and pager adapters.
  • iOS tests cover NSRunloopSequencer, the UIKit registrations, ViewModelViewHost, RoutedViewHost, ReactiveViewController activation, AppSupportJsonSuspensionDriver and IndexNormalizer.
  • The project is in reactiveui.slnx, so every CI leg builds it. ci-build.yml and sonarcloud.yml leave it out of the host test run with !tests/ReactiveUI.Device.Tests/**.
  • device-tests.yml runs both heads through the new actions-common device-tests workflow. It uses workflow-common-device-tests.yml@main.

Why

Nothing exercised the code under Platforms/android, apple-common and uikit-common or ReactiveUI.AndroidX, because it only runs on a device.

Breaking changes

None. Only tests and CI change.

How this was verified

The Android head ran on a local emulator, in Debug and Release, through the same device-tests script CI runs.

  • The iOS head builds on Windows in Debug and Release. It could not run here: the simulator needs macOS, so its first run is this PR's device-tests (ios) job.
  • Run it locally with dotnet run --file ../actions-common/scripts/device-tests.cs -- android --project src/tests/ReactiveUI.Device.Tests/ReactiveUI.Device.Tests.csproj; the project README has the details.

Notes for the reviewer

Start with Runner/DeviceTestSession.cs and the two heads, Platforms/Android/TestInstrumentation.cs and Platforms/iOS/IosTestHost.cs; the test classes are straightforward.

  • Runner/ConsoleCancelKeyShim.cs works around a TUnit 1.69 bug. TUnit subscribes to Console.CancelKeyPress at the start of a run and skips that only in the browser, so every run on Android or iOS fails before the first test. The shim fills the console's signal registration with an inert one. It should go once TUnit guards Android and iOS; that needs an issue on TUnit.
  • The iOS head sets EnableTrimAnalyzer to false. The app is not trimmed (MtouchLink=None), so the analysis only reports the reflection paths the tests cover; the SDK's MSTest runner does the same. Please confirm that is acceptable under the no-suppression rule.
  • LayoutViewHost with ResolveStrategy.ExplicitOptOut always throws. The host's own public View property is a writable View, so the opt-out strategy tries to wire it by name and fails. The tests cover opt-out on a plain view instead; the host bug is left for a separate fix.
  • Two more behaviours look wrong and are left as they are. ReactiveActivity<TViewModel>'s IViewFor.ViewModel setter assigns the field without raising a change, unlike the other hosts. ContextExtensions.ServiceBound disposes the context it was given when the subscription ends, so the tests pass it a ContextWrapper.
  • Out of scope: the *Unsafe view hosts, which are not on main yet (fix!: make every default view host AOT safe on Binding 8.1.0 #4505), and UsbManagerExtensions, which needs USB hardware. The device run collects no coverage: the coverage collector instruments a desktop test process.

Checklist

  • I have read the Contribute guide
  • The PR title follows Conventional Commits
  • Tests cover this change, or the summary says why they do not
  • New or changed public API has XML documentation

@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.62%. Comparing base (6def5a6) to head (dbd3035).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4506   +/-   ##
=======================================
  Coverage   93.62%   93.62%           
=======================================
  Files         175      175           
  Lines        6227     6227           
  Branches      743      743           
=======================================
  Hits         5830     5830           
  Misses        396      396           
  Partials        1        1           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Replace the stale ReactiveUI.NonParallel.Mobile.Tests (xUnit runners, missing project reference) with ReactiveUI.Device.Tests
- Run TUnit in-process on the device: an Android instrumentation relays results to `dotnet test --device`, an iOS app delegate writes a TRX report and exit code
- Cover HandlerSequencer, WithAndroidX registrations, PlatformOperations, activation, ControlFetcherMixins, LayoutViewHost, ReactiveViewHost, AutoSuspendHelper, BundleSuspensionDriver, SharedPreferences, service binding and the AndroidX adapters on a real emulator
- Cover NSRunloopSequencer, UIKit registrations, ViewModelViewHost, RoutedViewHost, ReactiveViewController, AppSupportJsonSuspensionDriver and IndexNormalizer on the iOS simulator
- Work around TUnit subscribing to Console.CancelKeyPress, which throws on Android and iOS
- Add the project to the solution and exclude it from the host test runs in ci-build.yml and sonarcloud.yml
- Add device-tests.yml, which calls the actions-common device-tests workflow
- Create the window from a scene delegate, since iOS 26 obsoletes the screen-bounds UIWindow constructor
- Start UIKit from a top-level entry point, which TUnit's Main-method analyzer accepts
- Turn off trimming and trim analysis for the untrimmed iOS app, and drop the Mono-only interpreter setting
- Target net11.0-android37, the Android framework the libraries ship, so no extra library build is needed
…window tests

ViewModelViewHost owns the controllers it resolves and disposes the one it
replaces. ReplacingViewModel_SwapsTheChildController read ParentViewController on
that disposed controller, which sent a message to a released native object and
crashed the app with SIGSEGV. The test now checks the old controller through
managed state and the host's own hierarchy.

The tests that show controllers share the app's one window, so they run under a
NotInParallel key; parallel runs replaced each other's root controller midway.

The iOS head keeps the trim analyzer on. TestViewController passes its view model
changes to WhenActivated, so it uses the trim-safe overload.
@glennawatson
glennawatson enabled auto-merge (squash) September 26, 2026 12:09
@sonarqubecloud

Copy link
Copy Markdown

@glennawatson
glennawatson merged commit dde34a2 into main Sep 26, 2026
17 of 18 checks passed
@glennawatson
glennawatson deleted the test/device-tests branch September 26, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants