diff --git a/.gitignore b/.gitignore index 65109106dc..d473df8f3a 100644 --- a/.gitignore +++ b/.gitignore @@ -144,4 +144,5 @@ project.fragment.lock.json /samples/Playground/ # jetbrains temp -.idea/ \ No newline at end of file +.idea/ +.DS_Store diff --git a/DEVICE_TESTING.md b/DEVICE_TESTING.md new file mode 100644 index 0000000000..22b97415ee --- /dev/null +++ b/DEVICE_TESTING.md @@ -0,0 +1,390 @@ +# Device Testing Implementation Status + +## Goal +Make `dotnet test` work exactly like `dotnet run` for device projects: + +```bash +# Current dotnet run (works in .NET 11): +dotnet run --project MyTests.csproj -f net10.0-android --device emulator-5554 + +# Goal for dotnet test: +dotnet test --project MyTests.csproj -f net10.0-android --device emulator-5554 +``` + +## Quick Start + +### Prerequisites + +1. **Install the .NET SDK** (done automatically by restore): + ```bash + ./restore.sh + ``` + +2. **Install Android workloads**: + ```bash + ./eng/install-workloads.sh android + ``` + +3. **Start an Android emulator** or connect a device: + ```bash + # List available devices + adb devices + ``` + +### Running Device Tests + +```bash +# Activity Mode (default) +dotnet test samples/public/BlankAndroid/BlankAndroid.csproj \ + -f net10.0-android \ + -p:DeviceId=emulator-5554 + +# Instrumentation Mode (more reliable) +dotnet test samples/public/BlankAndroid/BlankAndroid.csproj \ + -f net10.0-android \ + -p:DeviceId=emulator-5554 \ + -p:UseInstrumentation=true +``` + +## Current Status: ✅ Working with Two Modes + +The implementation supports **two modes** for running tests on devices: + +### Mode 1: Activity Mode (Default) - via `dotnet run --device` + +Uses `dotnet run --device` to deploy and launch the app's MainActivity. + +```bash +dotnet test BlankAndroid.csproj -f net10.0-android \ + -p:DeviceId=emulator-5554 +``` + +### Mode 2: Instrumentation Mode - via `adb instrument` + +Uses Android Instrumentation for more reliable test execution with proper wait-for-completion. + +```bash +dotnet test BlankAndroid.csproj -f net10.0-android \ + -p:DeviceId=emulator-5554 \ + -p:UseInstrumentation=true +``` + +### Test Output +``` +# ✓ Passed: SimpleTest_ShouldPass +# ✓ Passed: AndroidPlatformTest +# ✓ Passed: StringTest_ShouldPass +# ✓ Passed: LongRunningTest_30Seconds +# +# Test run summary: Passed! +# total: 4 +# failed: 0 +# succeeded: 4 +# skipped: 0 +# duration: 30s 282ms +``` + +## What Works ✅ + +| Feature | Status | Implementation | +|---------|--------|----------------| +| Build device test project | ✅ | Standard MSBuild | +| Deploy to device/emulator | ✅ | Via `dotnet run --device` or `dotnet build -t:Install` | +| Execute tests on device | ✅ | MainActivity (Activity mode) or TestInstrumentation (Instrumentation mode) | +| **Long-running tests** | ✅ | App runs until tests complete, then exits | +| Test results to logcat | ✅ | `IDataConsumer` MTP extension | +| Session start/end events | ✅ | `ITestSessionLifetimeHandler` | +| Pass/Fail/Error output | ✅ | Streamed via logcat | +| Exit code propagation | ✅ | Via `Java.Lang.JavaSystem.Exit()` or `Instrumentation.Finish()` | +| **TRX file collection** | ✅ | `adb shell run-as ... cat` | +| **Logcat collection** | ✅ | `adb logcat -d` saved to TestResults | +| **Workload check** | ✅ | Fails with helpful message if Android workload not installed | + +## What's Missing ❌ + +| Feature | Status | Blocker | +|---------|--------|---------| +| `--device` CLI argument | ❌ | Needs SDK change to `dotnet test` | +| `--project` CLI argument | ❌ | Needs SDK change to `dotnet test` | +| `--list-devices` argument | ❌ | Needs SDK change (already in `dotnet run`) | + +## Architecture + +### MSBuild Integration + +Device testing targets are designed to be split across SDKs: + +1. **Platform-specific targets** (Android/iOS) - will live in the respective SDK repos (dotnet/android, dotnet/maui) +2. **Common MTP targets** - remain in `Microsoft.Testing.Platform.MSBuild` package + +Currently for development, the Android targets are in `samples/public/BlankAndroid/Sdk.DeviceTesting.Android.targets`. + +``` +samples/public/BlankAndroid/ +├── Sdk.DeviceTesting.Android.targets # Android device testing targets (→ dotnet/android SDK) +├── Directory.Build.targets # Local imports for development +├── BlankAndroid.csproj # Sample test project +└── ... +``` + +When a project targets `net*-android` and has `IsTestProject=true`, these targets will be automatically imported by the Android SDK. + +### MSBuild Properties + +| Property | Description | Default | +|----------|-------------|---------| +| `DeviceId` | Device/emulator ID (e.g., `emulator-5554`) | `$(DEVICE_ID)` env var | +| `UseInstrumentation` | Use Android Instrumentation mode | `false` | +| `AndroidInstrumentationName` | Instrumentation class name | `$(RootNamespace.ToLower()).TestInstrumentation` | + +### Activity Mode (Default) + +``` +dotnet test BlankAndroid.csproj -f net10.0-android -p:DeviceId=emulator-5554 + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ MSBuild: Sdk.DeviceTesting.Android.targets │ +│ - Detects device TFM (net10.0-android) │ +│ - Checks Android workload is installed │ +│ - Sets UseMSBuildTestInfrastructure=true │ +│ - Overrides VSTest target │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ _RunAndroidTestsViaDotnetRun Target │ +│ - Executes: dotnet run --project -f --device │ +│ - dotnet run handles: build, deploy, run, logcat streaming │ +│ - Waits for app to exit │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ MainActivity.cs (App Entry Point) │ +│ - OnCreate → MicrosoftTestingPlatformEntryPoint.Main() │ +│ - Runs all tests │ +│ - Java.Lang.JavaSystem.Exit(exitCode) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ MTP Extensions (DeviceTestReporter.cs) │ +│ - IDataConsumer: Logs test results to logcat │ +│ - ITestSessionLifetimeHandler: Session events │ +│ - IOutputDeviceDataProducer: Formatted output │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ _CollectAndroidTestResults Target │ +│ - adb shell run-as ... ls -t files/TestResults/ │ +│ - adb shell run-as ... cat │ +│ - adb logcat -d > _logcat.txt │ +│ - Saves to bin/Debug/net10.0-android/TestResults/ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Instrumentation Mode (`-p:UseInstrumentation=true`) + +``` +dotnet test BlankAndroid.csproj -f net10.0-android -p:DeviceId=emulator-5554 -p:UseInstrumentation=true + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ MSBuild: Sdk.DeviceTesting.Android.targets │ +│ - Detects device TFM (net10.0-android) │ +│ - UseInstrumentation=true → delegates to adb instrument │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ _RunAndroidTestsViaInstrumentation Target │ +│ 1. dotnet build -t:Install (builds & deploys APK) │ +│ 2. adb shell am instrument -w │ +│ -w flag waits for instrumentation to finish │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ TestInstrumentation.cs (Instrumentation Entry Point) │ +│ - OnCreate → Start() │ +│ - OnStart → MicrosoftTestingPlatformEntryPoint.Main() │ +│ - Runs all tests │ +│ - Finish(exitCode, results) signals completion │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Key Files + +### Android Device Testing (→ dotnet/android SDK) + +| File | Purpose | Future Location | +|------|---------|-----------------| +| `samples/public/BlankAndroid/Sdk.DeviceTesting.Android.targets` | All Android device testing MSBuild logic | `dotnet/android` SDK | + +### Sample Project: samples/public/BlankAndroid/ + +| File | Purpose | +|------|---------| +| `BlankAndroid.csproj` | Simple test project with `IsTestProject=true` | +| `Sdk.DeviceTesting.Android.targets` | Android device testing targets (to be moved to dotnet/android SDK) | +| `Directory.Build.targets` | Local dev import of Sdk.DeviceTesting.Android.targets | +| `MainActivity.cs` | Activity mode entry point | +| `TestInstrumentation.cs` | Instrumentation mode entry point | +| `DeviceTestReporter.cs` | MTP extensions for logcat output | +| `DeviceTests.cs` | Sample MSTest tests | + +### Workload Installation Scripts + +| File | Purpose | +|------|---------| +| `eng/install-workloads.sh` | Install Android/iOS workloads (macOS/Linux) | +| `eng/install-workloads.ps1` | Install Android/iOS workloads (Windows) | + +## Creating a Device Test Project + +### Minimal Project File + +```xml + + + net10.0-android + Exe + com.example.MyTests + + + true + + + true + true + true + + + true + false + + + + + + + + + + + +``` + +### Required App Components + +1. **MainActivity.cs** - Entry point for Activity mode +2. **TestInstrumentation.cs** - Entry point for Instrumentation mode (optional) +3. **DeviceTestReporter.cs** - MTP extensions for logcat output (optional but recommended) + +## Usage + +### Setup (One-time) +```bash +# 1. Install the .NET SDK +./restore.sh + +# 2. Install Android workloads +./eng/install-workloads.sh android +``` + +### Activity Mode (Default) +```bash +# With MSBuild properties +dotnet test BlankAndroid.csproj -f net10.0-android \ + -p:DeviceId=emulator-5554 + +# With environment variables +export DEVICE_ID=emulator-5554 +dotnet test BlankAndroid.csproj -f net10.0-android +``` + +### Instrumentation Mode +```bash +# Enables more reliable test completion detection +dotnet test BlankAndroid.csproj -f net10.0-android \ + -p:DeviceId=emulator-5554 \ + -p:UseInstrumentation=true +``` + +### Goal (CLI arguments - requires SDK changes) +```bash +dotnet test --project BlankAndroid.csproj -f net10.0-android --device emulator-5554 +``` + +## Path to Success + +### ✅ Phase 1: COMPLETE - Working Prototype +- [x] MSBuild targets in Microsoft.Testing.Platform.MSBuild package +- [x] Auto-detection of device TFMs (android/ios) +- [x] Activity mode via `dotnet run --device` +- [x] Instrumentation mode via `adb instrument` +- [x] MTP test execution on device +- [x] Test result reporting via logcat +- [x] TRX file collection from device +- [x] Logcat collection for debugging +- [x] Workload installation scripts +- [x] Workload check before running tests + +### 🔄 Phase 2: IN PROGRESS - CLI Parity with `dotnet run` + +**Required:** Add `--device` and `--project` flags to `dotnet test` CLI + +```bash +# dotnet run (works today in .NET 11): +dotnet run --project X.csproj -f net10.0-android --device emulator-5554 + +# dotnet test (goal): +dotnet test --project X.csproj -f net10.0-android --device emulator-5554 +``` + +### 📋 Phase 3: Future Enhancements +- [ ] `--list-devices` support (already in `dotnet run`) +- [ ] iOS support (same pattern with test host app) +- [ ] Code coverage collection from device + +## TRX and Logcat Collection + +### Output Files +After test execution: +``` +bin/Debug/net10.0-android/TestResults/ +├── BlankAndroid.trx # Standard TRX test results +└── BlankAndroid_logcat.txt # Full device logcat for debugging +``` + +### Collection Method +- **TRX:** `adb shell run-as cat files/TestResults/` +- **Logcat:** `adb logcat -d > TestResults/_logcat.txt` + +## Troubleshooting + +### "Android workload is not installed" +Run the workload installation script: +```bash +./eng/install-workloads.sh android +``` + +### "Device not found" +1. Check device is connected: `adb devices` +2. Specify the device ID: `-p:DeviceId=emulator-5554` + +### Build errors with Activity/Bundle not found +This can happen on first build with project references. Run the build twice or build from the repo root first. + +## References + +- [MAUI Device Testing Spec](https://github.com/dotnet/maui/pull/33117) +- [Microsoft.Testing.Platform](https://aka.ms/mtp-overview) +- [dotnet run --device (.NET 11)](https://github.com/dotnet/sdk) +- [Android Instrumentation](https://developer.android.com/reference/android/app/Instrumentation) + +--- +**Last Updated:** 2026-01-16 +**Status:** ✅ Working prototype with Android device testing targets ready for migration to dotnet/android SDK diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 3b1ac4732f..31666a7926 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -25,5 +25,26 @@ https://github.com/microsoft/testfx f2ca2cf2507b769ba4e6609683619243588ee4c1 + + + https://github.com/dotnet/android + 097742b3d08fcf96472e20f05794779a4b05157a + + + https://github.com/dotnet/macios + 13be6a67ad7fb5968639d67a357bd04f892ed381 + + + https://github.com/dotnet/macios + 13be6a67ad7fb5968639d67a357bd04f892ed381 + + + https://github.com/dotnet/macios + 13be6a67ad7fb5968639d67a357bd04f892ed381 + + + https://github.com/dotnet/macios + 13be6a67ad7fb5968639d67a357bd04f892ed381 + diff --git a/eng/Versions.props b/eng/Versions.props index 7ef9b1169f..29a3cd5eaa 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -12,5 +12,17 @@ 4.1.0-preview.26065.7 2.1.0-preview.26065.7 + + 36.1.99-ci.main.92 + + 26.2.11233-ci.net11-0 + 26.2.11233-ci.net11-0 + 26.2.11233-ci.net11-0 + 26.2.11233-ci.net11-0 + $(MicrosoftMacCatalystSdknet110_262PackageVersion) + $(MicrosoftmacOSSdknet110_262PackageVersion) + $(MicrosoftiOSSdknet110_262PackageVersion) + $(MicrosofttvOSSdknet110_262PackageVersion) + diff --git a/eng/install-workloads.ps1 b/eng/install-workloads.ps1 new file mode 100644 index 0000000000..585c865f2a --- /dev/null +++ b/eng/install-workloads.ps1 @@ -0,0 +1,176 @@ +# Script to install dotnet workloads for device testing (Android, iOS, etc.) +# This script should be run after the SDK is installed in .dotnet folder. +# +# The script reads workload versions from eng/Versions.props to ensure +# consistent versions across the repository. +# +# Usage: +# .\eng\install-workloads.ps1 [workloads...] +# +# Examples: +# .\eng\install-workloads.ps1 # Install default workloads (android) +# .\eng\install-workloads.ps1 android # Install android workload with dependencies +# .\eng\install-workloads.ps1 android maui # Install specific workloads + +[CmdletBinding()] +param( + [Parameter(Position = 0, ValueFromRemainingArguments = $true)] + [string[]]$Workloads +) + +$ErrorActionPreference = 'Stop' + +$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptRoot + +# Default workloads to install if none specified +$defaultWorkloads = @('android') + +if ($Workloads.Count -eq 0) { + $Workloads = $defaultWorkloads +} + +# Find the dotnet installation +$dotnetRoot = Join-Path $repoRoot '.dotnet' + +if (-not (Test-Path $dotnetRoot)) { + Write-Error "Error: .dotnet folder not found at $dotnetRoot" + Write-Error "Please run .\restore.cmd or .\build.cmd first to install the SDK." + exit 1 +} + +$dotnetExe = Join-Path $dotnetRoot 'dotnet.exe' + +if (-not (Test-Path $dotnetExe)) { + Write-Error "Error: dotnet executable not found at $dotnetExe" + exit 1 +} + +# Read versions from eng/Versions.props +$versionsProps = Join-Path $scriptRoot 'Versions.props' + +function Get-VersionFromProps { + param([string]$PropertyName) + + if (Test-Path $versionsProps) { + $content = Get-Content $versionsProps -Raw + if ($content -match "<$PropertyName>([^<]+)") { + return $matches[1] + } + } + return $null +} + +function Resolve-PropertyReference { + param([string]$Value) + + if ($Value -match '\$\(([^)]+)\)') { + $refProp = $matches[1] + return Get-VersionFromProps -PropertyName $refProp + } + return $Value +} + +# Get workload versions from Versions.props +$androidVersion = Get-VersionFromProps -PropertyName 'MicrosoftAndroidSdkWindowsPackageVersion' +$iosVersion = Resolve-PropertyReference (Get-VersionFromProps -PropertyName 'MicrosoftiOSSdkPackageVersion') +$maccatalystVersion = Resolve-PropertyReference (Get-VersionFromProps -PropertyName 'MicrosoftMacCatalystSdkPackageVersion') + +Write-Host "Using dotnet from: $dotnetExe" +$sdkVersion = & $dotnetExe --version +Write-Host "SDK version: $sdkVersion" +Write-Host "" +Write-Host "Workload versions from eng/Versions.props:" +if ($androidVersion) { Write-Host " Android: $androidVersion" } +if ($iosVersion) { Write-Host " iOS: $iosVersion" } +if ($maccatalystVersion) { Write-Host " Mac Catalyst: $maccatalystVersion" } +Write-Host "" + +# Expand workload aliases to include dependencies and versions +# Format: "workload[@version]" +$expandedWorkloads = @() +foreach ($workload in $Workloads) { + switch ($workload) { + 'android' { + # Android workload requires wasm-tools for some scenarios + $expandedWorkloads += 'wasm-tools-net10' + if ($androidVersion) { + $expandedWorkloads += "android@$androidVersion" + } else { + $expandedWorkloads += 'android' + } + } + 'ios' { + if ($iosVersion) { + $expandedWorkloads += "ios@$iosVersion" + } else { + $expandedWorkloads += 'ios' + } + } + 'maccatalyst' { + if ($maccatalystVersion) { + $expandedWorkloads += "maccatalyst@$maccatalystVersion" + } else { + $expandedWorkloads += 'maccatalyst' + } + } + 'maui' { + # MAUI includes android, ios, and other dependencies + $expandedWorkloads += 'wasm-tools-net10' + $expandedWorkloads += 'maui' + } + default { + $expandedWorkloads += $workload + } + } +} + +# Remove duplicates while preserving order (compare base workload name without version) +$seen = @{} +$uniqueWorkloads = @() +foreach ($workload in $expandedWorkloads) { + $baseWorkload = $workload -replace '@.*$', '' + if (-not $seen.ContainsKey($baseWorkload)) { + $seen[$baseWorkload] = $true + $uniqueWorkloads += $workload + } +} + +# Install each workload +foreach ($workloadSpec in $uniqueWorkloads) { + # Parse workload@version format + $parts = $workloadSpec -split '@', 2 + $workload = $parts[0] + $version = if ($parts.Length -gt 1) { $parts[1] } else { $null } + + Write-Host "Installing workload: $workload" + if ($version) { + Write-Host " Version: $version" + } + + try { + $installArgs = @('workload', 'install', $workload, '--skip-sign-check') + if ($version) { + $installArgs += '--version' + $installArgs += $version + } + + & $dotnetExe @installArgs + if ($LASTEXITCODE -ne 0) { + Write-Warning "Warning: Failed to install workload '$workload' with version. Trying without specific version..." + & $dotnetExe workload install $workload --skip-sign-check + if ($LASTEXITCODE -ne 0) { + Write-Warning "Warning: Failed to install workload '$workload'. It may not be available for this SDK version." + } + } + } + catch { + Write-Warning "Warning: Failed to install workload '$workload': $_" + } + Write-Host "" +} + +Write-Host "Workload installation complete." +Write-Host "" +Write-Host "Installed workloads:" +& $dotnetExe workload list diff --git a/eng/install-workloads.sh b/eng/install-workloads.sh new file mode 100755 index 0000000000..60b50ef0bf --- /dev/null +++ b/eng/install-workloads.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash + +# Script to install dotnet workloads for device testing (Android, iOS, etc.) +# This script should be run after the SDK is installed in .dotnet folder. +# +# The script reads workload versions from eng/Versions.props to ensure +# consistent versions across the repository. +# +# Usage: +# ./eng/install-workloads.sh [workloads...] +# +# Examples: +# ./eng/install-workloads.sh # Install default workloads (android) +# ./eng/install-workloads.sh android # Install android workload with dependencies +# ./eng/install-workloads.sh android ios maui # Install specific workloads + +set -e + +source="${BASH_SOURCE[0]}" + +# Resolve script directory +while [[ -h "$source" ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + [[ $source != /* ]] && source="$scriptroot/$source" +done +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" +repo_root="$( cd -P "$scriptroot/.." && pwd )" + +# Default workloads to install if none specified +default_workloads="android" + +# Parse arguments +if [[ $# -eq 0 ]]; then + workloads="$default_workloads" +else + workloads="$*" +fi + +# Find the dotnet installation +dotnet_root="${repo_root}/.dotnet" + +if [[ ! -d "$dotnet_root" ]]; then + echo "Error: .dotnet folder not found at $dotnet_root" + echo "Please run ./restore.sh or ./build.sh first to install the SDK." + exit 1 +fi + +dotnet_exe="$dotnet_root/dotnet" + +if [[ ! -f "$dotnet_exe" ]]; then + echo "Error: dotnet executable not found at $dotnet_exe" + exit 1 +fi + +# Read versions from eng/Versions.props +versions_props="${scriptroot}/Versions.props" + +get_version_from_props() { + local property_name=$1 + if [[ -f "$versions_props" ]]; then + # Extract the value between the XML tags using sed (more portable than grep -P) + local value=$(sed -n "s/.*<${property_name}>\([^<]*\)<\/${property_name}>.*/\1/p" "$versions_props" 2>/dev/null | head -1) + echo "$value" + fi +} + +# Get workload versions from Versions.props +android_version=$(get_version_from_props "MicrosoftAndroidSdkWindowsPackageVersion") +ios_version=$(get_version_from_props "MicrosoftiOSSdkPackageVersion") +# If the version references another property, try to resolve it +if [[ "$ios_version" == *'$('* ]]; then + # Extract the referenced property name + ref_prop=$(echo "$ios_version" | sed 's/.*\$(\([^)]*\)).*/\1/') + if [[ -n "$ref_prop" && "$ref_prop" != "$ios_version" ]]; then + ios_version=$(get_version_from_props "$ref_prop") + fi +fi +maccatalyst_version=$(get_version_from_props "MicrosoftMacCatalystSdkPackageVersion") +if [[ "$maccatalyst_version" == *'$('* ]]; then + ref_prop=$(echo "$maccatalyst_version" | sed 's/.*\$(\([^)]*\)).*/\1/') + if [[ -n "$ref_prop" && "$ref_prop" != "$maccatalyst_version" ]]; then + maccatalyst_version=$(get_version_from_props "$ref_prop") + fi +fi + +echo "Using dotnet from: $dotnet_exe" +echo "SDK version: $("$dotnet_exe" --version)" +echo "" +echo "Workload versions from eng/Versions.props:" +[[ -n "$android_version" ]] && echo " Android: $android_version" +[[ -n "$ios_version" ]] && echo " iOS: $ios_version" +[[ -n "$maccatalyst_version" ]] && echo " Mac Catalyst: $maccatalyst_version" +echo "" + +# Expand workload aliases to include dependencies and versions +# Build a space-separated list of "workload[@version]" +expanded_workloads="" +for workload in $workloads; do + case "$workload" in + android) + # Android workload requires wasm-tools for some scenarios + expanded_workloads="$expanded_workloads wasm-tools-net10" + if [[ -n "$android_version" ]]; then + expanded_workloads="$expanded_workloads android@${android_version}" + else + expanded_workloads="$expanded_workloads android" + fi + ;; + ios) + if [[ -n "$ios_version" ]]; then + expanded_workloads="$expanded_workloads ios@${ios_version}" + else + expanded_workloads="$expanded_workloads ios" + fi + ;; + maccatalyst) + if [[ -n "$maccatalyst_version" ]]; then + expanded_workloads="$expanded_workloads maccatalyst@${maccatalyst_version}" + else + expanded_workloads="$expanded_workloads maccatalyst" + fi + ;; + maui) + # MAUI includes android, ios, and other dependencies + expanded_workloads="$expanded_workloads wasm-tools-net10 maui" + ;; + *) + expanded_workloads="$expanded_workloads $workload" + ;; + esac +done + +# Remove duplicates while preserving order (compare base workload name without version) +unique_workloads="" +seen_workloads="" +for workload_spec in $expanded_workloads; do + base_workload="${workload_spec%%@*}" + if [[ ! " $seen_workloads " =~ " $base_workload " ]]; then + seen_workloads="$seen_workloads $base_workload" + unique_workloads="$unique_workloads $workload_spec" + fi +done + +# Install each workload +for workload_spec in $unique_workloads; do + # Parse workload@version format + workload="${workload_spec%%@*}" + version="${workload_spec#*@}" + if [[ "$version" == "$workload_spec" ]]; then + version="" + fi + + echo "Installing workload: $workload" + if [[ -n "$version" ]]; then + echo " Version: $version" + fi + + # Build install command + if [[ -n "$version" ]]; then + "$dotnet_exe" workload install "$workload" --skip-sign-check --version "$version" || { + echo "Warning: Failed to install workload '$workload' with version $version. Trying without specific version..." + "$dotnet_exe" workload install "$workload" --skip-sign-check || { + echo "Warning: Failed to install workload '$workload'. It may not be available for this SDK version." + } + } + else + "$dotnet_exe" workload install "$workload" --skip-sign-check || { + echo "Warning: Failed to install workload '$workload'. It may not be available for this SDK version." + } + fi + echo "" +done + +echo "Workload installation complete." +echo "" +echo "Installed workloads:" +"$dotnet_exe" workload list diff --git a/samples/public/BlankAndroid/AndroidManifest.xml b/samples/public/BlankAndroid/AndroidManifest.xml new file mode 100644 index 0000000000..0336e1dee6 --- /dev/null +++ b/samples/public/BlankAndroid/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/samples/public/BlankAndroid/BlankAndroid.csproj b/samples/public/BlankAndroid/BlankAndroid.csproj new file mode 100644 index 0000000000..8593af2970 --- /dev/null +++ b/samples/public/BlankAndroid/BlankAndroid.csproj @@ -0,0 +1,50 @@ + + + net10.0-android + 24 + Exe + enable + enable + com.companyname.BlankAndroid + 1 + 1.0 + partial + + + true + + + true + true + true + + + true + false + + + + + <_IsDeviceTestProject>true + true + + + + + + + + + + + + + + + + + Device Testing Extensions + BlankAndroid.DeviceTestingPlatformBuilderHook + + + \ No newline at end of file diff --git a/samples/public/BlankAndroid/DeviceTestReporter.cs b/samples/public/BlankAndroid/DeviceTestReporter.cs new file mode 100644 index 0000000000..7c71d80588 --- /dev/null +++ b/samples/public/BlankAndroid/DeviceTestReporter.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Android.Util; + +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.OutputDevice; +using Microsoft.Testing.Platform.Extensions.TestHost; +using Microsoft.Testing.Platform.OutputDevice; +using Microsoft.Testing.Platform.Services; + +namespace BlankAndroid; + +/// +/// Builder hook that registers device-specific extensions with the testing platform. +/// +public static class DeviceTestingPlatformBuilderHook +{ + public static void AddExtensions(ITestApplicationBuilder builder, string[] args) + { + // Register the test result reporter (IDataConsumer) + builder.TestHost.AddDataConsumer(serviceProvider => + new DeviceTestReporter(serviceProvider.GetOutputDevice())); + + // Register the session lifetime handler + builder.TestHost.AddTestSessionLifetimeHandle(serviceProvider => + new DeviceTestSessionHandler(serviceProvider.GetOutputDevice())); + } +} + +/// +/// Custom data consumer that outputs test results to Android logcat. +/// Implements IDataConsumer to receive test node updates and IOutputDeviceDataProducer to output results. +/// +internal sealed class DeviceTestReporter : IDataConsumer, IOutputDeviceDataProducer +{ + private const string TAG = "MTP.TestResults"; + private readonly IOutputDevice _outputDevice; + private int _passed; + private int _failed; + private int _skipped; + + public Type[] DataTypesConsumed => [typeof(TestNodeUpdateMessage)]; + + public string Uid => nameof(DeviceTestReporter); + + public string Version => "1.0.0"; + + public string DisplayName => "Device Test Reporter"; + + public string Description => "Reports test results to Android logcat for device testing scenarios."; + + public DeviceTestReporter(IOutputDevice outputDevice) + { + _outputDevice = outputDevice; + } + + public async Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + { + var testNodeUpdateMessage = (TestNodeUpdateMessage)value; + string testDisplayName = testNodeUpdateMessage.TestNode.DisplayName; + TestNodeUid testId = testNodeUpdateMessage.TestNode.Uid; + + TestNodeStateProperty nodeState = testNodeUpdateMessage.TestNode.Properties.Single(); + + switch (nodeState) + { + case InProgressTestNodeStateProperty: + Log.Info(TAG, $"▶ Running: {testDisplayName}"); + await _outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"▶ Running: {testDisplayName}") + { + ForegroundColor = new SystemConsoleColor { ConsoleColor = ConsoleColor.Cyan } + }, cancellationToken); + break; + + case PassedTestNodeStateProperty: + _passed++; + Log.Info(TAG, $"✓ Passed: {testDisplayName}"); + await _outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"✓ Passed: {testDisplayName}") + { + ForegroundColor = new SystemConsoleColor { ConsoleColor = ConsoleColor.Green } + }, cancellationToken); + break; + + case FailedTestNodeStateProperty failedState: + _failed++; + string errorMessage = failedState.Exception?.Message ?? "Unknown error"; + Log.Info(TAG, $"✗ Failed: {testDisplayName}"); + Log.Info(TAG, $" Error: {errorMessage}"); + await _outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"✗ Failed: {testDisplayName}\n Error: {errorMessage}") + { + ForegroundColor = new SystemConsoleColor { ConsoleColor = ConsoleColor.Red } + }, cancellationToken); + break; + + case ErrorTestNodeStateProperty errorState: + _failed++; + string errMsg = errorState.Exception?.Message ?? "Unknown error"; + Log.Info(TAG, $"✗ Error: {testDisplayName}"); + Log.Info(TAG, $" Message: {errMsg}"); + await _outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"✗ Error: {testDisplayName}\n Message: {errMsg}") + { + ForegroundColor = new SystemConsoleColor { ConsoleColor = ConsoleColor.Red } + }, cancellationToken); + break; + + case SkippedTestNodeStateProperty: + _skipped++; + Log.Info(TAG, $"○ Skipped: {testDisplayName}"); + await _outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"○ Skipped: {testDisplayName}") + { + ForegroundColor = new SystemConsoleColor { ConsoleColor = ConsoleColor.Yellow } + }, cancellationToken); + break; + + default: + // Log unexpected state for debugging + Log.Info(TAG, $"? Unknown state for: {testDisplayName} - {nodeState.GetType().Name}"); + break; + } + } + + public Task IsEnabledAsync() => Task.FromResult(true); +} + +/// +/// Test session lifetime handler that outputs summary at the start and end of test sessions. +/// +internal sealed class DeviceTestSessionHandler : ITestSessionLifetimeHandler, IOutputDeviceDataProducer +{ + private const string TAG = "MTP.TestSession"; + private readonly IOutputDevice _outputDevice; + private DateTime _startTime; + + public string Uid => nameof(DeviceTestSessionHandler); + + public string Version => "1.0.0"; + + public string DisplayName => "Device Test Session Handler"; + + public string Description => "Handles test session lifecycle for device testing."; + + public DeviceTestSessionHandler(IOutputDevice outputDevice) + { + _outputDevice = outputDevice; + } + + public async Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) + { + _startTime = DateTime.Now; + string message = $""" + + ╔══════════════════════════════════════════════════════════════╗ + ║ Microsoft.Testing.Platform - Device Tests ║ + ╠══════════════════════════════════════════════════════════════╣ + ║ Started: {_startTime:yyyy-MM-dd HH:mm:ss} ║ + ╚══════════════════════════════════════════════════════════════╝ + + """; + Log.Info(TAG, message); + await _outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData(message) + { + ForegroundColor = new SystemConsoleColor { ConsoleColor = ConsoleColor.Cyan } + }, testSessionContext.CancellationToken); + } + + public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext) + { + var duration = DateTime.Now - _startTime; + string message = $""" + + ══════════════════════════════════════════════════════════════ + Test Run Completed + Duration: {duration.TotalSeconds:F2}s + ══════════════════════════════════════════════════════════════ + + """; + Log.Info(TAG, message); + await _outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData(message) + { + ForegroundColor = new SystemConsoleColor { ConsoleColor = ConsoleColor.Cyan } + }, testSessionContext.CancellationToken); + } + + public Task IsEnabledAsync() => Task.FromResult(true); +} diff --git a/samples/public/BlankAndroid/DeviceTests.cs b/samples/public/BlankAndroid/DeviceTests.cs new file mode 100644 index 0000000000..2268741057 --- /dev/null +++ b/samples/public/BlankAndroid/DeviceTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Android.Util; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BlankAndroid.Tests; + +[TestClass] +public class DeviceTests +{ + private const string TAG = "DeviceTests"; + + [TestMethod] + public void SimpleTest_ShouldPass() + { + // Arrange + int a = 2; + int b = 3; + + // Act + int result = a + b; + + // Assert + Assert.AreEqual(5, result); + Log.Info(TAG, "SimpleTest_ShouldPass completed"); + } + + [TestMethod] + public void AndroidPlatformTest() + { + // Verify we're running on Android + Assert.IsTrue(OperatingSystem.IsAndroid(), "Should be running on Android"); + Log.Info(TAG, "AndroidPlatformTest completed"); + } + + [TestMethod] + public void StringTest_ShouldPass() + { + // Test string operations + string hello = "Hello"; + string world = "World"; + + string result = $"{hello}, {world}!"; + + Assert.AreEqual("Hello, World!", result); + Log.Info(TAG, "StringTest_ShouldPass completed"); + } + + [TestMethod] + public async Task LongRunningTest_30Seconds() + { + // This test takes approximately 30 seconds to complete + // Used to verify that the instrumentation properly waits for test completion + Log.Info(TAG, "LongRunningTest_30Seconds started - will take 30 seconds"); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + // Simulate work over 30 seconds with periodic logging + for (int i = 1; i <= 30; i++) + { + await Task.Delay(1000); + + if (i % 5 == 0) + { + Log.Info(TAG, $"LongRunningTest_30Seconds progress: {i}/30 seconds"); + } + } + + stopwatch.Stop(); + + Log.Info(TAG, $"LongRunningTest_30Seconds completed in {stopwatch.Elapsed.TotalSeconds:F1} seconds"); + + // Verify the test actually took approximately 30 seconds + Assert.IsTrue(stopwatch.Elapsed.TotalSeconds >= 29, "Test should take at least 29 seconds"); + } +} diff --git a/samples/public/BlankAndroid/Directory.Build.props b/samples/public/BlankAndroid/Directory.Build.props new file mode 100644 index 0000000000..f233cee2d3 --- /dev/null +++ b/samples/public/BlankAndroid/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + + + <_IsDeviceTestProject>true + true + + true + + diff --git a/samples/public/BlankAndroid/Directory.Build.targets b/samples/public/BlankAndroid/Directory.Build.targets new file mode 100644 index 0000000000..a48cebb0c3 --- /dev/null +++ b/samples/public/BlankAndroid/Directory.Build.targets @@ -0,0 +1,29 @@ + + + + + + <_IsDeviceTestProject>true + true + + + + + + + + + + diff --git a/samples/public/BlankAndroid/MainActivity.cs b/samples/public/BlankAndroid/MainActivity.cs new file mode 100644 index 0000000000..6fc36a3959 --- /dev/null +++ b/samples/public/BlankAndroid/MainActivity.cs @@ -0,0 +1,68 @@ +using Android.Util; + +namespace BlankAndroid; + +/// +/// Main activity that runs tests when the app is launched via 'dotnet run --device'. +/// Tests are executed using Microsoft.Testing.Platform and results are output to logcat. +/// +[Activity(Label = "@string/app_name", MainLauncher = true)] +public class MainActivity : Activity +{ + private const string TAG = "DeviceTests"; + + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + Log.Info(TAG, "MainActivity.OnCreate - starting test execution"); + + // Run tests on a background thread to avoid blocking the UI thread + _ = Task.Run(RunTestsAsync); + } + + private async Task RunTestsAsync() + { + int exitCode = 1; + + try + { + // Get writable directory for test results + var filesDir = FilesDir?.AbsolutePath ?? "/data/local/tmp"; + var testResultsDir = Path.Combine(filesDir, "TestResults"); + + Directory.CreateDirectory(testResultsDir); + + Log.Info(TAG, $"Test results directory: {testResultsDir}"); + + // Configure test arguments + var args = new[] + { + "--results-directory", testResultsDir, + "--report-trx" + }; + + Log.Info(TAG, "Starting test execution..."); + + // Run the tests via the generated entry point + exitCode = await MicrosoftTestingPlatformEntryPoint.Main(args); + + Log.Info(TAG, $"Tests completed with exit code: {exitCode}"); + } + catch (Exception ex) + { + Log.Error(TAG, $"Test execution error: {ex}"); + exitCode = 1; + } + finally + { + Log.Info(TAG, $"Finishing activity with exit code: {exitCode}"); + + // Finish the activity and exit the process + RunOnUiThread(() => + { + FinishAffinity(); + Java.Lang.JavaSystem.Exit(exitCode); + }); + } + } +} \ No newline at end of file diff --git a/samples/public/BlankAndroid/Resources/AboutResources.txt b/samples/public/BlankAndroid/Resources/AboutResources.txt new file mode 100644 index 0000000000..219f42544b --- /dev/null +++ b/samples/public/BlankAndroid/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.xml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.xml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "Resource" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the Resource class would expose: + +public class Resource { + public class Drawable { + public const int icon = 0x123; + } + + public class Layout { + public const int main = 0x456; + } + + public class Strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use Resource.Drawable.icon to reference the drawable/icon.png file, or +Resource.Layout.main to reference the layout/main.xml file, or Resource.Strings.first_string +to reference the first string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/samples/public/BlankAndroid/Resources/layout/activity_main.xml b/samples/public/BlankAndroid/Resources/layout/activity_main.xml new file mode 100644 index 0000000000..f94985291b --- /dev/null +++ b/samples/public/BlankAndroid/Resources/layout/activity_main.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/samples/public/BlankAndroid/Resources/mipmap-anydpi-v26/appicon.xml b/samples/public/BlankAndroid/Resources/mipmap-anydpi-v26/appicon.xml new file mode 100644 index 0000000000..7751f69514 --- /dev/null +++ b/samples/public/BlankAndroid/Resources/mipmap-anydpi-v26/appicon.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/public/BlankAndroid/Resources/mipmap-anydpi-v26/appicon_round.xml b/samples/public/BlankAndroid/Resources/mipmap-anydpi-v26/appicon_round.xml new file mode 100644 index 0000000000..7751f69514 --- /dev/null +++ b/samples/public/BlankAndroid/Resources/mipmap-anydpi-v26/appicon_round.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon.png b/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon.png new file mode 100644 index 0000000000..0abfc1b581 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon_background.png b/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon_background.png new file mode 100644 index 0000000000..513e69d841 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon_background.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon_foreground.png b/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon_foreground.png new file mode 100644 index 0000000000..99d3a291bc Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-hdpi/appicon_foreground.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon.png b/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon.png new file mode 100644 index 0000000000..7b5a2e2bf8 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon_background.png b/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon_background.png new file mode 100644 index 0000000000..9e2d1e4d8d Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon_background.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon_foreground.png b/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon_foreground.png new file mode 100644 index 0000000000..a28d342c1c Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-mdpi/appicon_foreground.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon.png b/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon.png new file mode 100644 index 0000000000..b28b73c654 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon_background.png b/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon_background.png new file mode 100644 index 0000000000..658be3fb6a Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon_background.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon_foreground.png b/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon_foreground.png new file mode 100644 index 0000000000..70a542ac03 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xhdpi/appicon_foreground.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon.png b/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon.png new file mode 100644 index 0000000000..f9af1173ea Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon_background.png b/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon_background.png new file mode 100644 index 0000000000..9171c3e404 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon_background.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon_foreground.png b/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon_foreground.png new file mode 100644 index 0000000000..cb63bfb9f8 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xxhdpi/appicon_foreground.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon.png b/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon.png new file mode 100644 index 0000000000..1d948d6b5d Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon_background.png b/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon_background.png new file mode 100644 index 0000000000..1232d8c8d2 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon_background.png differ diff --git a/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon_foreground.png b/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon_foreground.png new file mode 100644 index 0000000000..9f9c9e6d93 Binary files /dev/null and b/samples/public/BlankAndroid/Resources/mipmap-xxxhdpi/appicon_foreground.png differ diff --git a/samples/public/BlankAndroid/Resources/values/ic_launcher_background.xml b/samples/public/BlankAndroid/Resources/values/ic_launcher_background.xml new file mode 100644 index 0000000000..6ec24e6413 --- /dev/null +++ b/samples/public/BlankAndroid/Resources/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #2C3E50 + \ No newline at end of file diff --git a/samples/public/BlankAndroid/Resources/values/strings.xml b/samples/public/BlankAndroid/Resources/values/strings.xml new file mode 100644 index 0000000000..38aefbe41a --- /dev/null +++ b/samples/public/BlankAndroid/Resources/values/strings.xml @@ -0,0 +1,4 @@ + + BlankAndroid + Hello, Android! + diff --git a/samples/public/BlankAndroid/Sdk.DeviceTesting.Android.targets b/samples/public/BlankAndroid/Sdk.DeviceTesting.Android.targets new file mode 100644 index 0000000000..b7a2e3b124 --- /dev/null +++ b/samples/public/BlankAndroid/Sdk.DeviceTesting.Android.targets @@ -0,0 +1,248 @@ + + + + + + <_IsAndroidTestProject Condition="'$(TargetFramework)' != '' AND $(TargetFramework.Contains('android'))">true + <_IsDeviceTestProject Condition="'$(_IsAndroidTestProject)' == 'true'">true + + + + + + true + + + $(DEVICE_ID) + + + false + + + <_LowerRootNamespace>$(RootNamespace.ToLower()) + $(_LowerRootNamespace).TestInstrumentation + + + + + + + <_IsDeviceTestProject>true + + + + + + + <_WorkloadListOutput> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_DeviceArg Condition="'$(DeviceId)' != ''">--device $(DeviceId) + <_LocalTestResultsPath>$(OutputPath)TestResults + <_AdbDevice Condition="'$(DeviceId)' != ''">-s $(DeviceId) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_AdbDevice Condition="'$(DeviceId)' != ''">-s $(DeviceId) + <_LocalTestResultsPath>$(OutputPath)TestResults + <_InstrumentationClass>$(ApplicationId)/$(AndroidInstrumentationName) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_AdbDevice Condition="'$(DeviceId)' != ''">-s $(DeviceId) + <_DeviceTestResultsPath>files/TestResults + <_LocalTestResultsPath>$(OutputPath)TestResults + <_LogcatFileName>$(MSBuildProjectName)_logcat.txt + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/public/BlankAndroid/TestInstrumentation.cs b/samples/public/BlankAndroid/TestInstrumentation.cs new file mode 100644 index 0000000000..ed595538cf --- /dev/null +++ b/samples/public/BlankAndroid/TestInstrumentation.cs @@ -0,0 +1,94 @@ +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Util; + +namespace BlankAndroid; + +/// +/// Android Instrumentation class for running tests. +/// This class is invoked via: adb shell am instrument -w com.companyname.BlankAndroid/blankandroid.TestInstrumentation +/// +[Instrumentation(Name = "blankandroid.TestInstrumentation")] +public class TestInstrumentation : Instrumentation +{ + private const string TAG = "TestInstrumentation"; + + // Required constructor for Android .NET interop + public TestInstrumentation(IntPtr handle, JniHandleOwnership transfer) + : base(handle, transfer) + { + } + + public override void OnCreate(Bundle? arguments) + { + base.OnCreate(arguments); + Log.Info(TAG, "TestInstrumentation.OnCreate called"); + + // Start the instrumentation - this will call OnStart + Start(); + } + + public override async void OnStart() + { + base.OnStart(); + Log.Info(TAG, "TestInstrumentation.OnStart called - running tests"); + + int exitCode = 1; + Bundle results = new Bundle(); + + try + { + // Get writable directory for test results + var context = TargetContext; + var filesDir = context?.FilesDir?.AbsolutePath ?? "/data/local/tmp"; + var testResultsDir = Path.Combine(filesDir, "TestResults"); + + Directory.CreateDirectory(testResultsDir); + + Log.Info(TAG, $"Test results directory: {testResultsDir}"); + + // Configure test arguments + var args = new[] + { + "--results-directory", testResultsDir, + "--report-trx" + }; + + Log.Info(TAG, "Starting test execution via MicrosoftTestingPlatformEntryPoint.Main"); + + // Run the tests + exitCode = await MicrosoftTestingPlatformEntryPoint.Main(args); + + Log.Info(TAG, $"Tests completed with exit code: {exitCode}"); + + results.PutInt("exitCode", exitCode); + results.PutString("testResultsDir", testResultsDir); + + if (exitCode == 0) + { + results.PutString("status", "SUCCESS"); + } + else + { + results.PutString("status", "FAILURE"); + } + } + catch (Exception ex) + { + Log.Error(TAG, $"Test execution error: {ex}"); + results.PutString("status", "ERROR"); + results.PutString("error", ex.ToString()); + exitCode = 1; + } + finally + { + Log.Info(TAG, $"Finishing instrumentation with exit code: {exitCode}"); + + // Signal completion - exitCode 0 = success, non-zero = failure + // The second parameter is the result code + Finish(exitCode == 0 ? Result.Ok : Result.Canceled, results); + } + } +} diff --git a/samples/public/TestingPlatformExamples/TestingPlatformExplorer/TestingPlatformExplorer.csproj b/samples/public/TestingPlatformExamples/TestingPlatformExplorer/TestingPlatformExplorer.csproj index 3d456723c8..4ccd9ed4db 100644 --- a/samples/public/TestingPlatformExamples/TestingPlatformExplorer/TestingPlatformExplorer.csproj +++ b/samples/public/TestingPlatformExamples/TestingPlatformExplorer/TestingPlatformExplorer.csproj @@ -1,4 +1,4 @@ - + Exe @@ -32,4 +32,4 @@ - + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Platform.MSBuild/buildMultiTargeting/Microsoft.Testing.Platform.MSBuild.targets b/src/Platform/Microsoft.Testing.Platform.MSBuild/buildMultiTargeting/Microsoft.Testing.Platform.MSBuild.targets index a3c0b01a44..175e50f2e5 100644 --- a/src/Platform/Microsoft.Testing.Platform.MSBuild/buildMultiTargeting/Microsoft.Testing.Platform.MSBuild.targets +++ b/src/Platform/Microsoft.Testing.Platform.MSBuild/buildMultiTargeting/Microsoft.Testing.Platform.MSBuild.targets @@ -258,10 +258,15 @@ <_SdkMajorVersion>$(NETCoreSdkVersion.Split('.')[0]) <_SupportsGlobalJsonTestRunner Condition="'$(NETCoreSdkVersion)' != '' AND '$(_SdkMajorVersion)' >= '10'">true + + <_IsDeviceTestProject Condition="'$(TargetFramework.Contains('android'))' == 'true' OR '$(TargetFramework.Contains('ios'))' == 'true'">true + Condition="'$(IsTestingPlatformApplication)'=='true' AND '$(TargetFramework)'!='' AND '$(_SupportsGlobalJsonTestRunner)'=='true' AND '$(_IsDeviceTestProject)'!='true'" /> + +