Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ Microsoft.Testing.Extensions.TrxReport.Abstractions.Streaming.TrxResultStreaming
Microsoft.Testing.Extensions.TrxReport.Abstractions.Streaming.TrxResultStreamingStore.TrxResultStreamingStore(string! filePath, Microsoft.Testing.Platform.Helpers.IFileSystem! fileSystem, Microsoft.Testing.Platform.Helpers.ITask! task, Microsoft.Testing.Platform.Logging.ILogger! logger, int batchSize, int flushIntervalMs, bool useBackgroundWriter) -> void
Microsoft.Testing.Platform.IPC.NamedPipeServer.NamedPipeServer(Microsoft.Testing.Platform.IPC.PipeNameDescription! pipeNameDescription, System.Func<Microsoft.Testing.Platform.IPC.IRequest!, System.Threading.Tasks.Task<Microsoft.Testing.Platform.IPC.IResponse!>!>! callback, Microsoft.Testing.Platform.Helpers.IEnvironment! environment, Microsoft.Testing.Platform.Logging.ILogger! logger, Microsoft.Testing.Platform.Helpers.ITask! task, int maxNumberOfServerInstances, System.Collections.Generic.IReadOnlyList<string!>? authorizedSecurityIdentities, System.Threading.CancellationToken cancellationToken) -> void
Microsoft.Testing.Platform.IPC.NamedPipeServer.NamedPipeServer(string! name, System.Func<Microsoft.Testing.Platform.IPC.IRequest!, System.Threading.Tasks.Task<Microsoft.Testing.Platform.IPC.IResponse!>!>! callback, Microsoft.Testing.Platform.Helpers.IEnvironment! environment, Microsoft.Testing.Platform.Logging.ILogger! logger, Microsoft.Testing.Platform.Helpers.ITask! task, System.Collections.Generic.IReadOnlyList<string!>? authorizedSecurityIdentities, System.Threading.CancellationToken cancellationToken) -> void
static Microsoft.Testing.Extensions.TrxReport.TrxModeHelpers.IsTestHostControllerSupported.get -> bool
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ This package extends Microsoft.Testing.Platform with:

Enable TRX report generation via the `--report-trx` command line option.

On platforms that can launch a test-host process (all except Android, browser, iOS, tvOS, and WASI), TRX uses controller-backed recovery by default: the test host still streams results and generates the report during normal execution, but a surviving controller process can recover completed results into a TRX report if the test host crashes, hangs, or is stopped by `--timeout`. Android, browser, iOS, tvOS, and WASI cannot launch a test-host process, so TRX automatically falls back to its original in-process implementation there — no controller-backed recovery is attempted, and no configuration is required to get this fallback.

The extra process has a measurable startup cost, which varies by target framework: for a trivial single-test run, launching the controller added no statistically measurable overhead on .NET (differences were within normal process-launch noise) but added roughly 700-800ms on .NET Framework in local measurements. Weigh this against the reliability benefit for your scenario, especially on .NET Framework or in tight inner-loop test runs.

## Related packages

- [Microsoft.Testing.Extensions.TrxReport.Abstractions](https://www.nuget.org/packages/Microsoft.Testing.Extensions.TrxReport.Abstractions): interfaces for extensions interoperating with TRX reports
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ public async Task OnTestSessionStartingAsync(ITestSessionContext testSessionCont
if (_logger.IsEnabled(LogLevel.Debug))
{
await _logger.LogDebugAsync($"""
CrashDumpCommandLineOptions.CrashDumpOptionName: {_commandLineOptionsService.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName)}
TrxReportGeneratorCommandLine.IsTrxReportEnabled: {_commandLineOptionsService.IsOptionSet(TrxReportGeneratorCommandLine.TrxReportOptionName)}
TrxModeHelpers.IsTestHostControllerSupported: {TrxModeHelpers.IsTestHostControllerSupported}
shouldUseOutOfProcessTrxGeneration: {shouldUseOutOfProcessTrxGeneration}
""").ConfigureAwait(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ public override Task<bool> IsEnabledAsync()
=> Task.FromResult(
// TrxReportGenerator is enabled only when trx report is enabled
_commandLineOptions.IsOptionSet(TrxReportGeneratorCommandLine.TrxReportOptionName)
// If crash dump is not enabled we run trx in-process only
&& TrxModeHelpers.ShouldUseOutOfProcessTrxGeneration(_commandLineOptions));
// TRX requires (and will trigger) a controller-managed test host whenever the current
// platform supports process restart; this is what makes plain --report-trx controller-backed.
&& TrxModeHelpers.IsTestHostControllerSupported);
#pragma warning restore SA1114 // Parameter list should follow declaration

protected override string GetMissingEnvironmentVariableErrorMessage(string environmentVariableName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,42 @@ namespace Microsoft.Testing.Extensions.TrxReport;

internal static class TrxModeHelpers
{
[UnsupportedOSPlatformGuard("BROWSER")]
// TRX requires a controller-managed test host to recover the report when the test host crashes,
// hangs, or is terminated by --timeout. Platforms that cannot launch a test-host process at all
// fall back to the in-process implementation as a compatibility fallback (see
// TrxReportExtensions.AddTrxReportProvider). Android cannot use the platform's default
// Process.Start-based launch path (TestHostControllersTestHost.ProcessLifecycle) to spawn an
// arbitrary child test host, so it must fall back in-process too even though the package targets
// it (see the csproj's SupportedPlatform entries for android/browser/ios/tvos/wasi).
//
// This assembly also ships a netstandard2.0 asset, so these checks must run for every target, not
// just NETCOREAPP: under NETCOREAPP they resolve via the BCL OperatingSystem APIs, and under
// netstandard2.0/.NET Framework they resolve via the Polyfills OperatingSystem extension, which
// itself already returns constant false for these platforms on .NET Framework (which cannot run on
// them) while evaluating the actual runtime via RuntimeInformation.IsOSPlatform for netstandard2.0
// hosts (e.g. Mono/MAUI) that can.
[UnsupportedOSPlatformGuard("android")]
[UnsupportedOSPlatformGuard("browser")]
[UnsupportedOSPlatformGuard("ios")]
[UnsupportedOSPlatformGuard("tvos")]
[UnsupportedOSPlatformGuard("wasi")]
public static bool IsTestHostControllerSupported { get; } =
Comment thread
Evangelink marked this conversation as resolved.
!OperatingSystem.IsAndroid()
&& !OperatingSystem.IsBrowser()
&& !OperatingSystem.IsIOS()
&& !OperatingSystem.IsTvOS()
&& !OperatingSystem.IsWasi();
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.

// Used from within the test host (child) process: rely on the controller-presence state that MTP
// actually established for this process, rather than recomputing which extension requested
// isolation. This stays true even when another extension (HangDump, --timeout, ...) is the one
// that caused the controller to be used.
Comment thread
Evangelink marked this conversation as resolved.
[UnsupportedOSPlatformGuard("android")]
[UnsupportedOSPlatformGuard("browser")]
[UnsupportedOSPlatformGuard("ios")]
[UnsupportedOSPlatformGuard("tvos")]
[UnsupportedOSPlatformGuard("wasi")]
public static bool ShouldUseOutOfProcessTrxGeneration(ICommandLineOptions commandLineOptions)
=> commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName) &&
!OperatingSystem.IsBrowser();
=> IsTestHostControllerSupported
&& commandLineOptions.IsOptionSet(PlatformCommandLineProvider.TestHostControllerPIDOptionKey);
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,17 @@ public Task<bool> IsEnabledAsync()
=> Task.FromResult(
// TrxReportGenerator is enabled only when trx report is enabled
_commandLineOptions.IsOptionSet(TrxReportGeneratorCommandLine.TrxReportOptionName)
// If crash dump is not enabled we run trx in-process only
&& TrxModeHelpers.ShouldUseOutOfProcessTrxGeneration(_commandLineOptions));
// TRX requires (and will trigger) a controller-managed test host whenever the current
// platform supports process restart; this is what makes plain --report-trx controller-backed.
&& TrxModeHelpers.IsTestHostControllerSupported);
#pragma warning restore SA1114 // Parameter list should follow declaration

public Task BeforeTestHostProcessStartAsync(CancellationToken cancellationToken)
{
// IsEnabledAsync will only return true if we are out of process.
// If we are not out of process, then we are disabled. Hence, this won't be called.
// The extra check is to let the platform compatibility analyzer know that we are not running in browser.
if (!TrxModeHelpers.ShouldUseOutOfProcessTrxGeneration(_commandLineOptions))
if (!TrxModeHelpers.IsTestHostControllerSupported)
{
throw ApplicationStateGuard.Unreachable();
}
Expand All @@ -117,7 +118,11 @@ public Task BeforeTestHostProcessStartAsync(CancellationToken cancellationToken)
return Task.CompletedTask;
}

[UnsupportedOSPlatform("BROWSER")]
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("tvos")]
[UnsupportedOSPlatform("wasi")]
private void BeforeTestHostProcessStartCore(CancellationToken cancellationToken)
{
_singleConnectionNamedPipeServer = new(_pipeNameDescription, CallbackAsync, _environment, _logger, _task, cancellationToken);
Expand Down Expand Up @@ -389,7 +394,7 @@ public async ValueTask DisposeAsync()

public void Dispose()
{
if (TrxModeHelpers.ShouldUseOutOfProcessTrxGeneration(_commandLineOptions))
if (TrxModeHelpers.IsTestHostControllerSupported)
{
_singleConnectionNamedPipeServer?.Dispose();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Testing.Extensions.TrxReport;
using Microsoft.Testing.Extensions.TrxReport.Abstractions;
using Microsoft.Testing.Extensions.TrxReport.Resources;
using Microsoft.Testing.Platform.Builder;
Expand All @@ -11,10 +12,6 @@
using Microsoft.Testing.Platform.Services;
using Microsoft.Testing.Platform.TestHostControllers;

#if !NETCOREAPP
using Polyfills;
#endif

namespace Microsoft.Testing.Extensions;

/// <summary>
Expand Down Expand Up @@ -53,9 +50,9 @@ public static void AddTrxReportProvider(this ITestApplicationBuilder builder)
serviceProvider.GetService<TrxTestApplicationLifecycleCallbacks>(),
serviceProvider.GetLoggerFactory().CreateLogger<TrxReportGenerator>()));

if (!OperatingSystem.IsBrowser())
if (TrxModeHelpers.IsTestHostControllerSupported)
{
NonBrowserRegistrations(builder);
ControllerBackedRegistrations(builder);
Comment thread
Evangelink marked this conversation as resolved.
}

builder.TestHost.AddDataConsumer(compositeTestSessionTrxService);
Expand All @@ -79,8 +76,12 @@ public static void AddTrxReportProvider(this ITestApplicationBuilder builder)
artifactPostProcessingBuilder.Tools.AddTool(serviceProvider => toolTrxMergeFactory.CreateTool(serviceProvider.GetCommandLineOptions()));
}

[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
private static void NonBrowserRegistrations(ITestApplicationBuilder builder)
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("tvos")]
[UnsupportedOSPlatform("wasi")]
private static void ControllerBackedRegistrations(ITestApplicationBuilder builder)
{
builder.TestHost.AddTestHostApplicationLifetime(serviceProvider =>
new TrxTestApplicationLifecycleCallbacks(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ namespace Microsoft.Testing.Extensions.TrxReport.Abstractions;

internal sealed class TrxTestApplicationLifecycleCallbacks : ITestHostApplicationLifetime, IDisposable
{
[UnsupportedOSPlatformGuard("BROWSER")]
[UnsupportedOSPlatformGuard("android")]
[UnsupportedOSPlatformGuard("browser")]
[UnsupportedOSPlatformGuard("ios")]
[UnsupportedOSPlatformGuard("tvos")]
[UnsupportedOSPlatformGuard("wasi")]
private readonly bool _isEnabled;

private readonly IEnvironment _environment;
Expand All @@ -26,7 +30,7 @@ public TrxTestApplicationLifecycleCallbacks(
_isEnabled =
// TrxReportGenerator is enabled only when trx report is enabled
commandLineOptionsService.IsOptionSet(TrxReportGeneratorCommandLine.TrxReportOptionName) &&
// If crash dump is not enabled we run trx in-process only
// The controller pipe is available only in a controller-launched test host.
TrxModeHelpers.ShouldUseOutOfProcessTrxGeneration(commandLineOptionsService);

_environment = environment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@
<target state="translated">Možnost --{0} od zprostředkovatele {1} (UID: {2}) používá vyhrazenou předponu --internal.</target>
<note>{0} is the option name without the leading dashes. {1} is the provider display name. {2} is the provider UID. {Locked="--internal"}</note>
</trans-unit>
<trans-unit id="CommandLineOptionRequiresExtension">
<source>Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</source>
<target state="new">Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</target>
<note>{0} is the option name without the leading dashes. {1} is the extension's NuGet package name.</note>
</trans-unit>
<trans-unit id="CommandLineOptionSuggestion">
<source>Did you mean '--{0}'?</source>
<target state="new">Did you mean '--{0}'?</target>
<note>{0} is the suggested option name without the leading dashes.</note>
</trans-unit>
<trans-unit id="CommandLineOptionsNotReady">
<source>The ICommandLineOptions has not been built yet.</source>
<target state="translated">Rozhraní ICommandLineOptions ještě není sestavené.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@
<target state="translated">Die Option "--{0}" vom Anbieter "{1}" (UID: {2}) verwendet das reservierte Präfix "--internal".</target>
<note>{0} is the option name without the leading dashes. {1} is the provider display name. {2} is the provider UID. {Locked="--internal"}</note>
</trans-unit>
<trans-unit id="CommandLineOptionRequiresExtension">
<source>Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</source>
<target state="new">Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</target>
<note>{0} is the option name without the leading dashes. {1} is the extension's NuGet package name.</note>
</trans-unit>
<trans-unit id="CommandLineOptionSuggestion">
<source>Did you mean '--{0}'?</source>
<target state="new">Did you mean '--{0}'?</target>
<note>{0} is the suggested option name without the leading dashes.</note>
</trans-unit>
<trans-unit id="CommandLineOptionsNotReady">
<source>The ICommandLineOptions has not been built yet.</source>
<target state="translated">ICommandLineOptions wurde noch nicht erstellt.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@
<target state="translated">La opción “--{0}” del proveedor “{1}” (UID: {2}) usa el prefijo reservado “--internal”</target>
<note>{0} is the option name without the leading dashes. {1} is the provider display name. {2} is the provider UID. {Locked="--internal"}</note>
</trans-unit>
<trans-unit id="CommandLineOptionRequiresExtension">
<source>Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</source>
<target state="new">Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</target>
<note>{0} is the option name without the leading dashes. {1} is the extension's NuGet package name.</note>
</trans-unit>
<trans-unit id="CommandLineOptionSuggestion">
<source>Did you mean '--{0}'?</source>
<target state="new">Did you mean '--{0}'?</target>
<note>{0} is the suggested option name without the leading dashes.</note>
</trans-unit>
<trans-unit id="CommandLineOptionsNotReady">
<source>The ICommandLineOptions has not been built yet.</source>
<target state="translated">ICommandLineOptions aún no se ha compilado.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@
<target state="translated">L’option « --{0} » du fournisseur « {1} » (UID : {2}) utilise le préfixe réservé « --internal »</target>
<note>{0} is the option name without the leading dashes. {1} is the provider display name. {2} is the provider UID. {Locked="--internal"}</note>
</trans-unit>
<trans-unit id="CommandLineOptionRequiresExtension">
<source>Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</source>
<target state="new">Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</target>
<note>{0} is the option name without the leading dashes. {1} is the extension's NuGet package name.</note>
</trans-unit>
<trans-unit id="CommandLineOptionSuggestion">
<source>Did you mean '--{0}'?</source>
<target state="new">Did you mean '--{0}'?</target>
<note>{0} is the suggested option name without the leading dashes.</note>
</trans-unit>
<trans-unit id="CommandLineOptionsNotReady">
<source>The ICommandLineOptions has not been built yet.</source>
<target state="translated">ICommandLineOptions n’a pas encore été généré.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@
<target state="translated">L'opzione '--{0}' del provider '{1}' (UID: {2}) usa il prefisso riservato '--internal'</target>
<note>{0} is the option name without the leading dashes. {1} is the provider display name. {2} is the provider UID. {Locked="--internal"}</note>
</trans-unit>
<trans-unit id="CommandLineOptionRequiresExtension">
<source>Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</source>
<target state="new">Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</target>
<note>{0} is the option name without the leading dashes. {1} is the extension's NuGet package name.</note>
</trans-unit>
<trans-unit id="CommandLineOptionSuggestion">
<source>Did you mean '--{0}'?</source>
<target state="new">Did you mean '--{0}'?</target>
<note>{0} is the suggested option name without the leading dashes.</note>
</trans-unit>
<trans-unit id="CommandLineOptionsNotReady">
<source>The ICommandLineOptions has not been built yet.</source>
<target state="translated">ICommandLineOptions non è stato ancora compilato.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@
<target state="translated">プロバイダー '{1}' のオプション '--{0}' (UID:{2}) は予約済みプレフィックス '--internal' を 使用しています</target>
<note>{0} is the option name without the leading dashes. {1} is the provider display name. {2} is the provider UID. {Locked="--internal"}</note>
</trans-unit>
<trans-unit id="CommandLineOptionRequiresExtension">
<source>Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</source>
<target state="new">Option '--{0}' is provided by the '{1}' extension. Add a package reference to use it.</target>
<note>{0} is the option name without the leading dashes. {1} is the extension's NuGet package name.</note>
</trans-unit>
<trans-unit id="CommandLineOptionSuggestion">
<source>Did you mean '--{0}'?</source>
<target state="new">Did you mean '--{0}'?</target>
<note>{0} is the suggested option name without the leading dashes.</note>
</trans-unit>
<trans-unit id="CommandLineOptionsNotReady">
<source>The ICommandLineOptions has not been built yet.</source>
<target state="translated">ICommandLineOptions はまだ構築されていません。</target>
Expand Down
Loading