Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0e2dd7c
fix: skip Ctrl+C handling where Console.CancelKeyPress is unsupported
glennawatson Sep 26, 2026
382603c
ci: execute a shared TUnit smoke test on niche .NET platforms
thomhurst Sep 26, 2026
f6d3bfe
ci: reuse runner Android tools and configure Apple smoke launches
thomhurst Sep 26, 2026
b3d55ea
ci: run Apple smoke tests on x64 simulators and invoke Catalyst directly
thomhurst Sep 26, 2026
1f2ee3e
ci: pass Catalyst test arguments through the platform launcher
thomhurst Sep 26, 2026
6b3937a
ci: bundle Apple interpreter dependencies and verify application comp…
thomhurst Sep 26, 2026
7475c3c
ci: quote the Apple completion check as a YAML block
thomhurst Sep 26, 2026
4a2d65b
ci: write Catalyst test results inside its sandbox container
thomhurst Sep 26, 2026
b457888
ci: wait for Apple simulators to finish booting before installing tests
thomhurst Sep 26, 2026
6cd356a
ci: execute Apple smoke apps directly with native platform tools
thomhurst Sep 26, 2026
74cb469
ci: keep Catalyst reports within its writable sandbox
thomhurst Sep 26, 2026
c6d0c1c
ci: capture native Catalyst output and WASI failure artifacts
thomhurst Sep 26, 2026
567a954
ci: disable external report writes in the Catalyst sandbox
thomhurst Sep 26, 2026
adf7142
fix: align console signal annotations with platform guards
thomhurst Sep 26, 2026
1f2364a
ci: run WASI smoke test on .NET 11
thomhurst Sep 26, 2026
e764e45
ci: use .NET 11 Apple SDK tests on ARM64 runners
thomhurst Sep 26, 2026
0d40693
ci: use .NET 11 SDK packaging and CoreCLR for mobile smoke tests
thomhurst Sep 26, 2026
61a9128
ci: adapt smoke launches to .NET 11 SDK defaults
thomhurst Sep 26, 2026
22f304b
ci: publish the .NET 11 browser app and correct Apple launch paths
thomhurst Sep 26, 2026
2bc5cfb
ci: clear the unused UIKit context before the generated test entry point
thomhurst Sep 26, 2026
3ae2f37
ci: provide simulator test reports through normal TUnit configuration
thomhurst Sep 26, 2026
3fa5ea9
ci: run simulator apps with the installed simctl and verify TRX results
thomhurst Sep 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions .github/workflows/platform-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: Platform smoke tests

on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
smoke:
name: ${{ matrix.platform }} smoke test
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- platform: Browser
os: ubuntu-latest
workload: wasm-tools
- platform: Android
os: ubuntu-latest
workload: android
- platform: iOS
os: macos-26
workload: ios
device: iPhone 17
simulator: iOS-26-5
- platform: tvOS
os: macos-26
workload: tvos
device: Apple TV
simulator: tvOS-26-5
- platform: MacCatalyst
os: macos-26
workload: maccatalyst
- platform: WASI
os: ubuntu-latest
workload: wasi-experimental
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
TESTINGPLATFORM_TELEMETRY_OPTOUT: 1
SmokePlatform: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-dotnet@v6.0.0
with:
dotnet-version: 11.0.100-rc.1.26425.128
- uses: actions/cache@v6
with:
path: ~/.nuget/packages
key: platform-smoke-${{ runner.os }}-${{ runner.arch }}-${{ matrix.platform }}-${{ hashFiles('global.json', 'Directory.Packages.props', 'tests/TUnit.PlatformSmoke/TUnit.PlatformSmoke.csproj') }}
restore-keys: platform-smoke-${{ runner.os }}-${{ runner.arch }}-${{ matrix.platform }}-
- name: Select the installed Xcode
if: runner.os == 'macOS'
run: sudo xcode-select --switch /Applications/Xcode_26.6.app
- name: Start the installed Apple simulator
if: matrix.platform == 'iOS' || matrix.platform == 'tvOS'
run: |
device=$(xcrun simctl list devices available --json | jq -er '.devices["com.apple.CoreSimulator.SimRuntime.${{ matrix.simulator }}"] | map(select(.name == "${{ matrix.device }}")) | first | .udid // error("Required preinstalled simulator is missing")')
xcrun simctl boot "$device"
echo "Device=$device" >> "$GITHUB_ENV"
- run: dotnet workload install ${{ matrix.workload }}
- name: Install XHarness
if: matrix.platform == 'Browser'
run: >-
dotnet tool install Microsoft.DotNet.XHarness.CLI --global
--version 10.0.0-prerelease.26471.3
--add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json
- name: Publish the browser smoke test
if: matrix.platform == 'Browser'
run: dotnet publish tests/TUnit.PlatformSmoke -c Debug
- name: Build the smoke test
if: matrix.platform != 'Browser'
run: dotnet build tests/TUnit.PlatformSmoke -c Debug
- name: Execute the TUnit smoke test in Chrome
if: matrix.platform == 'Browser'
run: >-
xharness wasm test-browser
--app=tests/TUnit.PlatformSmoke/bin/Debug/net11.0/publish/wwwroot
--browser=chrome --expected-exit-code=0 --timeout=00:02:00
--output-directory=artifacts/platform-smoke/browser
- name: Enable KVM
if: matrix.platform == 'Android'
run: sudo chmod 666 /dev/kvm
- name: Execute the TUnit smoke test on Android
if: matrix.platform == 'Android'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
target: google_apis
disable-animations: true
script: |
mkdir -p artifacts/platform-smoke/Android
bash -o pipefail -c 'dotnet test --project "$GITHUB_WORKSPACE/tests/TUnit.PlatformSmoke/TUnit.PlatformSmoke.csproj" --no-build 2>&1 | tee artifacts/platform-smoke/Android/test.log'
- name: Wait for the Apple simulator
if: matrix.platform == 'iOS' || matrix.platform == 'tvOS'
timeout-minutes: 10
run: |
xcrun simctl bootstatus "$Device" -b
- name: Execute the TUnit smoke test on Mac Catalyst
if: matrix.platform == 'MacCatalyst'
working-directory: tests/TUnit.PlatformSmoke
timeout-minutes: 10
shell: bash
run: |
mkdir -p "$GITHUB_WORKSPACE/artifacts/platform-smoke/${{ matrix.platform }}"
dotnet test --project "$GITHUB_WORKSPACE/tests/TUnit.PlatformSmoke/TUnit.PlatformSmoke.csproj" --no-build --minimum-expected-tests 1 \
2>&1 | tee "$GITHUB_WORKSPACE/artifacts/platform-smoke/${{ matrix.platform }}/test.log"
- name: Execute the TUnit smoke test in the Apple simulator
if: matrix.platform == 'iOS' || matrix.platform == 'tvOS'
timeout-minutes: 5
shell: bash
run: |
mkdir -p "artifacts/platform-smoke/${{ matrix.platform }}"
platform=$(echo "$SmokePlatform" | tr '[:upper:]' '[:lower:]')
app="tests/TUnit.PlatformSmoke/bin/Debug/net11.0-$platform/${platform}simulator-arm64/TUnit.PlatformSmoke.app"
xcrun simctl install "$Device" "$app"
container=$(xcrun simctl get_app_container "$Device" net.dot.TUnitSmoke data)
xcrun simctl launch --console "$Device" net.dot.TUnitSmoke --connection-mode=none \
--app-arg=--minimum-expected-tests --app-arg=1 \
--app-arg=--report-trx --app-arg=--report-trx-filename --app-arg=smoke.trx \
--app-arg=--results-directory "--app-arg=$container/Documents/TestResults" \
Comment thread
thomhurst marked this conversation as resolved.
2>&1 | tee "artifacts/platform-smoke/${{ matrix.platform }}/test.log"
test "$(xmllint --xpath 'boolean(/*[local-name()="TestRun"]/*[local-name()="ResultSummary"]/*[local-name()="Counters"][@total="1" and @passed="1" and @failed="0"])' "$container/Documents/TestResults/smoke.trx")" = true
- name: Collect Apple simulator test reports
if: always() && (matrix.platform == 'iOS' || matrix.platform == 'tvOS')
run: |
if container=$(xcrun simctl get_app_container "$Device" net.dot.TUnitSmoke data 2>/dev/null); then
if [[ -d "$container/Documents/TestResults" ]]; then
mkdir -p "artifacts/platform-smoke/${{ matrix.platform }}"
cp -R "$container/Documents/TestResults" "artifacts/platform-smoke/${{ matrix.platform }}/"
fi
fi
- uses: bytecodealliance/actions/wasmtime/setup@v1.1.3
if: matrix.platform == 'WASI'
with:
version: v49.0.1
- name: Execute the TUnit smoke test on WASI
if: matrix.platform == 'WASI'
working-directory: tests/TUnit.PlatformSmoke/bin/Debug/net11.0/wasi-wasm
shell: bash
run: |
mkdir -p "$GITHUB_WORKSPACE/artifacts/platform-smoke/WASI"
wasmtime run -S http --dir . dotnet.wasm TUnit.PlatformSmoke \
2>&1 | tee "$GITHUB_WORKSPACE/artifacts/platform-smoke/WASI/run.log"
- uses: actions/upload-artifact@v7.0.1
if: always()
with:
name: platform-smoke-${{ matrix.platform }}
path: artifacts/platform-smoke
if-no-files-found: ignore
43 changes: 41 additions & 2 deletions src/TUnit.Core/EngineCancellationToken.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.Versioning;
using TUnit.Core.Settings;

namespace TUnit.Core;
Expand All @@ -20,6 +21,7 @@ public class EngineCancellationToken : IDisposable
private int _initialised;
private volatile bool _forcefulExitStarted;
private CancellationTokenRegistration _platformRegistration;
private bool _cancelKeyPressSubscribed;

public EngineCancellationToken()
{
Expand Down Expand Up @@ -60,13 +62,45 @@ internal void Initialise(CancellationToken platformCancellationToken = default)
if (!OperatingSystem.IsBrowser())
{
#endif
Console.CancelKeyPress += OnCancelKeyPress;
_cancelKeyPressSubscribed = TrySubscribeCancelKeyPress();
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
#if NET5_0_OR_GREATER
}
#endif
}

/// <summary>
/// Subscribes to <see cref="Console.CancelKeyPress"/>. Platforms without console signals
/// (such as Android, iOS and tvOS) throw <see cref="PlatformNotSupportedException"/>; there the
/// run is cancelled only through the platform token.
/// </summary>
#if NET5_0_OR_GREATER
[UnsupportedOSPlatform("browser")]
#endif
private bool TrySubscribeCancelKeyPress()
Comment thread
thomhurst marked this conversation as resolved.
{
try
{
SubscribeCancelKeyPress();
return true;
}
catch (PlatformNotSupportedException)
{
return false;
}
}

// Virtual so tests can simulate a platform without console signals.
#if NET5_0_OR_GREATER
[UnsupportedOSPlatform("browser")]
#endif
internal virtual void SubscribeCancelKeyPress() => Console.CancelKeyPress += OnCancelKeyPress;

#if NET5_0_OR_GREATER
[UnsupportedOSPlatform("browser")]
#endif
internal virtual void UnsubscribeCancelKeyPress() => Console.CancelKeyPress -= OnCancelKeyPress;

private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
Cancel();
Expand Down Expand Up @@ -137,7 +171,12 @@ public void Dispose()
if (!OperatingSystem.IsBrowser())
{
#endif
Console.CancelKeyPress -= OnCancelKeyPress;
if (_cancelKeyPressSubscribed)
{
_cancelKeyPressSubscribed = false;
UnsubscribeCancelKeyPress();
}

AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
#if NET5_0_OR_GREATER
}
Expand Down
46 changes: 46 additions & 0 deletions tests/TUnit.PlatformSmoke/AndroidInstrumentation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#if ANDROID
using Android.App;
using Android.OS;
using Android.Runtime;

namespace TUnit.PlatformSmoke;

[Instrumentation(Name = "net.dot.TUnitSmoke.TestInstrumentation")]
public sealed class AndroidInstrumentation(IntPtr handle, JniHandleOwnership ownership)
: Instrumentation(handle, ownership)
{
public override void OnCreate(Bundle? arguments)
{
base.OnCreate(arguments);
Start();
}

public override async void OnStart()
{
base.OnStart();
using var result = new Bundle();
try
{
var resultsDirectory = Path.Combine(Application.Context.GetExternalFilesDir(null)!.AbsolutePath, "TestResults");
// Use the same generated entry point and registered hooks as every other platform.
var exitCode = await MicrosoftTestingPlatformEntryPoint.Main([
"--minimum-expected-tests", "1",
"--report-trx", "--report-trx-filename", "smoke.trx",
"--results-directory", resultsDirectory
]);
result.PutString("resultsPath", Path.Combine(resultsDirectory, "smoke.trx"));
if (exitCode != 0)
{
result.PutString("error", $"TUnit exited with code {exitCode}.");
}

Finish(exitCode == 0 ? Result.Ok : Result.Canceled, result);
}
catch (Exception exception)
{
result.PutString("error", exception.ToString());
Finish(Result.Canceled, result);
}
}
}
#endif
16 changes: 16 additions & 0 deletions tests/TUnit.PlatformSmoke/AppleInitialization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#if IOS || TVOS || MACCATALYST
using System.Runtime.CompilerServices;

namespace TUnit.PlatformSmoke;

internal static class AppleInitialization
{
[ModuleInitializer]
internal static void Initialize()
{
// The Apple SDK installs a UIKit context before Main. This console-style
// test app has no UI event loop to execute captured async continuations.
SynchronizationContext.SetSynchronizationContext(null);
}
}
#endif
33 changes: 33 additions & 0 deletions tests/TUnit.PlatformSmoke/SmokeTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using TUnit.Assertions;
using TUnit.Core;

namespace TUnit.PlatformSmoke;

public class SmokeTest
{
[Test]
public async Task RunsOnTheExpectedPlatform()
{
await Task.Yield();
await Assert.That(Environment.Version.Major).IsEqualTo(11);

#if BROWSER
await Assert.That(OperatingSystem.IsBrowser()).IsTrue();
#elif ANDROID
await Assert.That(OperatingSystem.IsAndroid()).IsTrue();
#elif IOS
await Assert.That(OperatingSystem.IsIOS()).IsTrue();
#elif TVOS
await Assert.That(OperatingSystem.IsTvOS()).IsTrue();
#elif MACCATALYST
await Assert.That(OperatingSystem.IsMacCatalyst()).IsTrue();
#elif WASI
await Assert.That(OperatingSystem.IsWasi()).IsTrue();
#endif
#if IOS || TVOS || MACCATALYST
await Assert.That(System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture)
.IsEqualTo(System.Runtime.InteropServices.Architecture.Arm64);
#endif
await Assert.That(new List<int> { 1, 2 }.Count).IsEqualTo(2);
}
}
59 changes: 59 additions & 0 deletions tests/TUnit.PlatformSmoke/TUnit.PlatformSmoke.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" Condition="'$(SmokePlatform)' != 'Browser'" />
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.WebAssembly" Condition="'$(SmokePlatform)' == 'Browser'" />
<Import Project="../../eng/TestProject.props" />

<PropertyGroup>
<TargetFrameworks />
<TargetFramework>net11.0</TargetFramework>
<TargetFramework Condition="'$(SmokePlatform)' == 'Android'">net11.0-android</TargetFramework>
<TargetFramework Condition="'$(SmokePlatform)' == 'iOS'">net11.0-ios</TargetFramework>
<TargetFramework Condition="'$(SmokePlatform)' == 'tvOS'">net11.0-tvos</TargetFramework>
<TargetFramework Condition="'$(SmokePlatform)' == 'MacCatalyst'">net11.0-maccatalyst</TargetFramework>
<EnableTUnitPolyfills>false</EnableTUnitPolyfills>
<GenerateTestingPlatformEntryPoint>true</GenerateTestingPlatformEntryPoint>
</PropertyGroup>

<PropertyGroup Condition="'$(SmokePlatform)' == 'Android' or '$(SmokePlatform)' == 'iOS' or '$(SmokePlatform)' == 'tvOS' or '$(SmokePlatform)' == 'MacCatalyst'">
<ApplicationId>net.dot.TUnitSmoke</ApplicationId>
<ApplicationTitle>TUnit smoke test</ApplicationTitle>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<RuntimeIdentifier Condition="'$(SmokePlatform)' == 'Android'">android-x64</RuntimeIdentifier>
<RuntimeIdentifier Condition="'$(SmokePlatform)' == 'iOS'">iossimulator-arm64</RuntimeIdentifier>
<RuntimeIdentifier Condition="'$(SmokePlatform)' == 'tvOS'">tvossimulator-arm64</RuntimeIdentifier>
<RuntimeIdentifier Condition="'$(SmokePlatform)' == 'MacCatalyst'">maccatalyst-arm64</RuntimeIdentifier>
<RunWithOpen>false</RunWithOpen>
<MtouchLink Condition="'$(SmokePlatform)' != 'Android'">None</MtouchLink>
</PropertyGroup>

<PropertyGroup Condition="'$(SmokePlatform)' == 'Android'">
<AndroidUseInstrumentation>true</AndroidUseInstrumentation>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>

<PropertyGroup Condition="'$(SmokePlatform)' == 'Browser' or '$(SmokePlatform)' == 'WASI'">
<RuntimeFrameworkVersion>11.0.0-rc.1.26425.128</RuntimeFrameworkVersion>
<UseMonoRuntime>true</UseMonoRuntime>
<SelfContained>true</SelfContained>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
<PropertyGroup Condition="'$(SmokePlatform)' == 'WASI'">
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<DefineConstants>$(DefineConstants);WASI</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(SmokePlatform)' == 'Browser'">
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<DefineConstants>$(DefineConstants);BROWSER</DefineConstants>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../../src/TUnit/TUnit.csproj" />
</ItemGroup>
<Import Project="../../eng/TestProject.targets" />
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" Condition="'$(SmokePlatform)' != 'Browser'" />
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.WebAssembly" Condition="'$(SmokePlatform)' == 'Browser'" />
</Project>
7 changes: 7 additions & 0 deletions tests/TUnit.PlatformSmoke/testconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"commandLineOptions": {
"minimum-expected-tests": 1,
"report-trx": true,
"results-directory": "TestResults"
}
}
Loading
Loading