Skip to content

Add --enable-coverage flag for code coverage collection from device tests#1565

Open
amirvenus wants to merge 4 commits intodotnet:mainfrom
amirvenus:feature/coverage-collection
Open

Add --enable-coverage flag for code coverage collection from device tests#1565
amirvenus wants to merge 4 commits intodotnet:mainfrom
amirvenus:feature/coverage-collection

Conversation

@amirvenus
Copy link

@amirvenus amirvenus commented Mar 15, 2026

Summary

Adds an optional --enable-coverage CLI flag across all platforms that enables code coverage collection during XHarness device test execution. When enabled, XHarness generates a Cobertura XML coverage report and places it in the output directory alongside test results.

No external coverage tools (coverlet, etc.) are required. CoverageManager generates method-level coverage by reflecting over the loaded test assemblies. Standard tools like coverlet.msbuild cannot instrument assemblies for device builds (APK/app bundles) since they only hook into the dotnet test pipeline — CoverageManager solves this by providing built-in coverage that works on all platforms.

If an external tool has already produced a coverage file at the expected path, CoverageManager uses that file instead.

Addresses #1135.

What's included

  • --enable-coverage CLI flag on all platform commands:

    • android test, android run
    • apple test, apple just-test
    • wasm test, wasm test-browser
  • Test Runner integration (TestRunners.Common):

    • NUNIT_ENABLE_COVERAGE / NUNIT_COVERAGE_OUTPUT_PATH environment variable support in ApplicationOptions
    • CoverageManager that generates method-level Cobertura XML via reflection over loaded test assemblies (no external dependencies)
    • Coverage lifecycle integrated into ApplicationEntryPoint.InternalRunAsync()
  • Per-platform transport:

    • Android: Coverage flag passed as instrumentation argument → test app bridges to env var → CoverageManager generates report → path reported via INSTRUMENTATION_RESULT: coverage-results-path=<path>InstrumentationRunner pulls file from device via adb
    • Apple (iOS/tvOS/macOS): NUNIT_ENABLE_COVERAGE injected as env var via the orchestrator → coverage file written to app's Documents directory → pulled alongside test results
    • WASM: Coverage data emitted on stdout using STARTCOVERAGEXML <len> <base64> ENDCOVERAGEXML markers → WasmTestMessagesProcessor decodes and writes to output directory
  • ThreadlessXunitTestRunner made public: Enables Android test apps to use the runner that works with .NET Android's assembly store (in-memory test discovery via ReflectionAssemblyInfo, no .dll file on disk required)

Usage

# Android
xharness android test --app=test.apk --package-name=com.example --enable-coverage --output-directory=./out

# iOS
xharness apple test --app=TestApp.app --target=ios-simulator-64 --enable-coverage --output-directory=./out

# WASM
xharness wasm test --enable-coverage --output-directory=./out -- test.js

After the run, coverage.cobertura.xml appears in the output directory. The file uses standard Cobertura format consumable by Azure DevOps, Codecov, ReportGenerator, etc.

Test plan

  • Unit tests for CoverageManager (8 tests) — constructor, path resolution, env var setting, reflection coverage generation, fallback behavior
  • All existing unit tests pass (97 TestRunners + 21 Android = 118 tests)
  • Android end-to-end: Built test APK with Android.OS.Build API tests → deployed to emulator via locally-built XHarness CLI with --enable-coverage → 5/5 tests passed → coverage.cobertura.xml (2,741 bytes) pulled from device to output directory
  • iOS end-to-end: Built iOS app with UIKit.UIDevice API tests → ran on iPhone 11 Pro simulator (iOS 26.2) via locally-built XHarness CLI with --enable-coverage → 6/6 tests passed → coverage.cobertura.xml generated in app container
  • CLI builds successfully for all platforms

🤖 Generated with Claude Code

…ests

Adds optional `--enable-coverage` CLI flag across all platforms (Android,
Apple/iOS/tvOS, WASM) that enables code coverage collection during test
execution. When enabled, XHarness coordinates coverage output paths and
transports the resulting Cobertura XML files from the device back to the
host output directory alongside test results.

Changes:
- New `--enable-coverage` switch argument shared across all platforms
- `NUNIT_ENABLE_COVERAGE` / `NUNIT_COVERAGE_OUTPUT_PATH` env var support
  in ApplicationOptions for test runner configuration
- CoverageManager that generates method-level Cobertura XML via reflection
  when no external coverage tool (coverlet) produces a file
- Android: coverage flag passed as instrumentation arg, InstrumentationRunner
  pulls coverage-results-path from device via adb
- Apple: coverage env vars injected into app environment for simulator/device
- WASM: coverage data decoded from STARTCOVERAGEXML stdout markers
- ThreadlessXunitTestRunner made public (enables Android apps to use the
  runner that works with assembly store, required for device testing)
- Unit tests for CoverageManager

Closes dotnet#1135
CoverageManager generates coverage via reflection — no external tool
required. Standard tools like coverlet.msbuild cannot instrument
assemblies for device builds (APK/app bundles) since they only hook
into the dotnet test pipeline.
@amirvenus
Copy link
Author

The 2 failing checks (E2E Apple - iOS devices and E2E Apple - tvOS devices) are pre-existing failures on main — the ios-device-System.Buffers.Tests.app Helix work item fails identically on the main branch. These are infrastructure/device-lab issues, not related to this PR.

All 12 checks relevant to our changes pass: builds (Windows/OSX, Debug/Release), Android E2E (devices, simulators, manual commands), Apple E2E (simulators, simulator commands, device commands), and WASM E2E.

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.

1 participant