Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions samples/BrowserPlayground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,37 @@ countdown-event wait, and the adapter's per-test task factory.
Console output is routed through `BrowserOutputDevice`, which forwards to the browser's
`globalThis.console.*` APIs via `[JSImport]`.

Long-running tests are surfaced as durable diagnostic lines after 60 seconds, with
exponential backoff (60 seconds, 2 minutes, 4 minutes, and so on):

```console
[slow] still running after 1m 00s: MyLongRunningTest
```

Set `MTP_PROGRESS_SLOW_TEST_SECONDS` to a non-negative integer to change the first
reporting threshold; `0` disables these diagnostics. The browser launcher reads it from
the page query string:

```text
index.html?MTP_PROGRESS_SLOW_TEST_SECONDS=5
```

The Node launcher forwards the process environment variable into the managed WebAssembly
runtime:

```powershell
$env:MTP_PROGRESS_SLOW_TEST_SECONDS = '5'
node runtests.mjs
```

Test starts are tracked silently, so this does not duplicate normal per-test progress output.

The reporter is cooperative: it uses asynchronous delays and does not create a thread,
timer thread, or process. It can therefore identify an asynchronously suspended test,
because control returns to the browser event loop. It cannot report while a test
synchronously blocks the sole WebAssembly thread; no managed timer or continuation can
run until that test yields or returns.

### Feature matrix (what is *not* available on browser-wasm)

Several extensions/capabilities are intentionally unavailable in the browser sandbox and
Expand Down
14 changes: 11 additions & 3 deletions samples/BrowserPlayground/wwwroot/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@
// Microsoft.Testing.Platform.
import { dotnet } from './_framework/dotnet.js';

const { runMain } = await dotnet
const slowTestThresholdEnvironmentVariable = 'MTP_PROGRESS_SLOW_TEST_SECONDS';
const runtimeBuilder = dotnet
// A browser page has no argv, so Microsoft.Testing.Platform command-line options are
// taken from the page's query string, e.g.:
// index.html?arg=--minimum-expected-tests&arg=1
.withApplicationArgumentsFromQuery()
.create();
.withApplicationArgumentsFromQuery();

const slowTestThreshold = new URLSearchParams(globalThis.location.search)
.get(slowTestThresholdEnvironmentVariable);
if (slowTestThreshold !== null) {
runtimeBuilder.withEnvironmentVariable(slowTestThresholdEnvironmentVariable, slowTestThreshold);
}

const { runMain } = await runtimeBuilder.create();

// runMain() invokes Program.Main and resolves to its exit code (0 = all tests passed,
// non-zero = failures / policy violation).
Expand Down
11 changes: 8 additions & 3 deletions samples/BrowserPlayground/wwwroot/runtests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@
// e.g. node runtests.mjs --minimum-expected-tests 1
import { dotnet } from './_framework/dotnet.js';

const { runMain } = await dotnet
.withApplicationArguments(...process.argv.slice(2))
.create();
const slowTestThresholdEnvironmentVariable = 'MTP_PROGRESS_SLOW_TEST_SECONDS';
const runtimeBuilder = dotnet.withApplicationArguments(...process.argv.slice(2));
const slowTestThreshold = process.env[slowTestThresholdEnvironmentVariable];
if (slowTestThreshold !== undefined) {
runtimeBuilder.withEnvironmentVariable(slowTestThresholdEnvironmentVariable, slowTestThreshold);
}

const { runMain } = await runtimeBuilder.create();

// runMain() boots the same bundle under Node and resolves to the exit code of the sample's
// Program.Main (defined by the top-level statements in Program.cs).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ static Microsoft.Testing.Extensions.RunSettingsProviderHelper.CanReadFile(Micros
static Microsoft.Testing.Extensions.RunSettingsProviderHelper.FindInvalidTestParameter(string![]! arguments) -> string?
static Microsoft.Testing.Extensions.RunSettingsProviderHelper.HasEnvironmentVariables(System.Xml.Linq.XDocument! runSettings) -> bool
static Microsoft.Testing.Extensions.RunSettingsProviderHelper.TryLoadRunSettingsAsync(Microsoft.Testing.Platform.CommandLine.ICommandLineOptions! commandLineOptions, Microsoft.Testing.Platform.Helpers.IFileSystem! fileSystem, Microsoft.Testing.Platform.Helpers.IEnvironment! environment, string! runSettingsOptionName) -> System.Threading.Tasks.Task<System.Xml.Linq.XDocument?>!
static Microsoft.VisualStudio.TestTools.UnitTesting.MSTestTestNodeConverter.ToEmptyResultTestNode(Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel.UnitTestElement! element, bool isTrxEnabled) -> Microsoft.Testing.Platform.Extensions.Messages.TestNode!
Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.MSTestEngine
Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.MSTestEngine.Cancel() -> void
Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.MSTestEngine.DiscoverAsync(System.Collections.Generic.IEnumerable<string!>! sources, string? settingsXml, Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.IAdapterMessageLogger! logger, Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.IUnitTestElementSink! elementSink, Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ITestElementFilterProvider? filterProvider, Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.IConfiguration? configuration, Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ITestSourceHandler! testSourceHandler, bool isMTP) -> System.Threading.Tasks.Task!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ public static TestNode ToInProgressTestNode(UnitTestElement element, bool isTrxE
return testNode;
}

/// <summary>
/// Builds a state-neutral node update that signals execution completed without producing a result.
/// </summary>
public static TestNode ToEmptyResultTestNode(UnitTestElement element, bool isTrxEnabled)
{
TestNode testNode = CreateBaseTestNode(element, isTrxEnabled, displayNameOverride: null, out _);
testNode.Properties.Add(TestNodeExecutionCompletedProperty.CachedInstance);
Comment thread
Evangelink marked this conversation as resolved.
return testNode;
}

/// <summary>
/// Builds a completed <see cref="TestNode"/> carrying the outcome, timing, output and (optionally) TRX
/// properties for a single executed test result.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,8 @@ public Task RecordStartAsync(UnitTestElement testElement)
return PublishAsync(testNode);
}

// Mirrors the VSTest recorder: an empty result maps to RecordEnd(TestOutcome.None), which does not publish any
// test node update to Microsoft.Testing.Platform. So there is nothing to report here.
public Task RecordEmptyResultAsync(UnitTestElement testElement)
=> Task.CompletedTask;
=> PublishAsync(MSTestTestNodeConverter.ToEmptyResultTestNode(testElement, _isTrxEnabled));
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.

public async Task<bool> RecordResultAsync(UnitTestElement testElement, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,27 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella
}

TestNodeStateProperty? state = update.TestNode.Properties.SingleOrDefault<TestNodeStateProperty>();
if (state is null)
bool executionCompleted = update.TestNode.Properties.Any<TestNodeExecutionCompletedProperty>();
if (state is null && !executionCompleted)
{
return;
}

string? line = state switch
{
InProgressTestNodeStateProperty => FormatLine(StartedEvent, _clock.UtcNow, update.TestNode.Uid, update.TestNode.DisplayName),
string? line = executionCompleted
? FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Completed")
: state switch
{
InProgressTestNodeStateProperty => FormatLine(StartedEvent, _clock.UtcNow, update.TestNode.Uid, update.TestNode.DisplayName),
#pragma warning disable CS0618, MTP0001 // Type or member is obsolete - keep parity with HangDumpActivityIndicator's terminal-state set.
PassedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Passed"),
FailedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Failed"),
ErrorTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Error"),
SkippedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Skipped"),
CancelledTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Cancelled"),
TimeoutTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Timeout"),
PassedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Passed"),
FailedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Failed"),
ErrorTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Error"),
SkippedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Skipped"),
CancelledTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Cancelled"),
TimeoutTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Timeout"),
#pragma warning restore CS0618, MTP0001
_ => null,
};
_ => null,
};

if (line is null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella
}

TestNodeStateProperty? state = nodeChangedMessage.TestNode.Properties.SingleOrDefault<TestNodeStateProperty>();
bool executionCompleted = nodeChangedMessage.TestNode.Properties.Any<TestNodeExecutionCompletedProperty>();
if (state is InProgressTestNodeStateProperty)
{
if (_traceLevelEnabled)
Expand All @@ -140,9 +141,10 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella
_testsCurrentExecutionState.TryAdd(nodeChangedMessage.TestNode.Uid, (nodeChangedMessage.TestNode.DisplayName, typeof(InProgressTestNodeStateProperty), _clock.UtcNow));
}
#pragma warning disable CS0618, MTP0001 // Type or member is obsolete
else if (state is PassedTestNodeStateProperty or ErrorTestNodeStateProperty or CancelledTestNodeStateProperty
else if ((executionCompleted
|| state is PassedTestNodeStateProperty or ErrorTestNodeStateProperty or CancelledTestNodeStateProperty
#pragma warning restore CS0618, MTP0001 // Type or member is obsolete
or FailedTestNodeStateProperty or TimeoutTestNodeStateProperty or SkippedTestNodeStateProperty
or FailedTestNodeStateProperty or TimeoutTestNodeStateProperty or SkippedTestNodeStateProperty)
&& _testsCurrentExecutionState.TryRemove(nodeChangedMessage.TestNode.Uid, out (string Name, Type Type, DateTimeOffset StartTime) record)
&& _traceLevelEnabled)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo
return Task.CompletedTask;
}

string testUid = update.TestNode.Uid.Value;
if (update.TestNode.Properties.Any<TestNodeExecutionCompletedProperty>())
{
lock (_stateGate)
{
_inFlight.Remove(testUid);
}

TryPruneOldSegments();
return Task.CompletedTask;
}

// A node carries a single state property in practice; use FirstOrDefault rather than
// SingleOrDefault so a malformed producer can't throw out of the consumer pump.
TestNodeStateProperty? state = update.TestNode.Properties.FirstOrDefault<TestNodeStateProperty>();
Expand All @@ -25,8 +37,6 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo
return Task.CompletedTask;
}

string testUid = update.TestNode.Uid.Value;

if (state is InProgressTestNodeStateProperty)
{
lock (_stateGate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalProgressMessageState.Te
Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalProgressMessageState.Version.get -> long
Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalTestReporter.UpdateProgressMessage(string! executionId, string! instanceId, string! producerUid, string! key, string? message) -> void
Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalTestReporter.ClearProgressMessages() -> void
Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalTestReporter.TestCompletedWithoutResult(string! executionId, string! testNodeUid) -> void
Microsoft.Testing.Platform.Telemetry.OpenTelemetryResultHandler.NotifyExecutionCompleted(Microsoft.Testing.Platform.Extensions.Messages.TestNode! testNode) -> void
virtual Microsoft.Testing.Platform.OutputDevice.SimplifiedConsoleOutputDeviceBase.DisplayActiveTestProgress.get -> bool
static Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalTestReporterCommandLineOptionsProvider.IsProgressEnabled(Microsoft.Testing.Platform.CommandLine.ICommandLineOptions! commandLineOptions) -> bool
const Microsoft.Testing.Platform.OutputDevice.Terminal.TestProgressStateAwareTerminal.MaximumVisibleProgressMessages = 5 -> int
Expand Down Expand Up @@ -271,3 +273,28 @@ Microsoft.Testing.Platform.OutputDevice.Terminal.TerminalTestReporter.TerminalTe
Microsoft.Testing.Platform.OutputDevice.Terminal.TestProgressStateAwareTerminal.TestProgressStateAwareTerminal(Microsoft.Testing.Platform.OutputDevice.Terminal.ITerminal! terminal, System.Func<bool?>! showProgress, Microsoft.Testing.Platform.OutputDevice.Terminal.IProgressRenderer! renderer, Microsoft.Testing.Platform.Logging.ILogger! logger) -> void
Microsoft.Testing.Platform.ServerMode.ServerModeManager.MessageHandlerFactory.MessageHandlerFactory(string! host, int port, Microsoft.Testing.Platform.OutputDevice.IOutputDevice! outputDevice, Microsoft.Testing.Platform.Logging.ILogger! logger) -> void
Microsoft.Testing.Platform.ServerMode.TcpMessageHandler.TcpMessageHandler(System.Net.Sockets.TcpClient! client, System.IO.Stream! clientToServerStream, System.IO.Stream! serverToClientStream, Microsoft.Testing.Platform.ServerMode.IMessageFormatter! formatter, Microsoft.Testing.Platform.Logging.ILogger! logger) -> void
const Microsoft.Testing.Platform.OutputDevice.ProgressReportingConfiguration.MTP_PROGRESS_SILENCE_SECONDS = "MTP_PROGRESS_SILENCE_SECONDS" -> string!
const Microsoft.Testing.Platform.OutputDevice.ProgressReportingConfiguration.MTP_PROGRESS_SLOW_TEST_SECONDS = "MTP_PROGRESS_SLOW_TEST_SECONDS" -> string!
Microsoft.Testing.Platform.OutputDevice.ActiveTestTracker
Microsoft.Testing.Platform.OutputDevice.ActiveTestTracker.ActiveTestTracker(System.TimeSpan slowTestThreshold, System.Func<Microsoft.Testing.Platform.Helpers.IStopwatch!>! createStopwatch) -> void
Microsoft.Testing.Platform.OutputDevice.ActiveTestTracker.Clear() -> void
Microsoft.Testing.Platform.OutputDevice.ActiveTestTracker.Complete(Microsoft.Testing.Platform.Extensions.Messages.TestNodeUid! uid) -> void
Microsoft.Testing.Platform.OutputDevice.ActiveTestTracker.GetDueDiagnostics() -> Microsoft.Testing.Platform.OutputDevice.SlowTestDiagnostic![]!
Microsoft.Testing.Platform.OutputDevice.ActiveTestTracker.IsActive(Microsoft.Testing.Platform.OutputDevice.SlowTestDiagnostic! diagnostic) -> bool
Microsoft.Testing.Platform.OutputDevice.ActiveTestTracker.IsEnabled.get -> bool
Microsoft.Testing.Platform.OutputDevice.ActiveTestTracker.Start(Microsoft.Testing.Platform.Extensions.Messages.TestNodeUid! uid, string! displayName) -> void
Microsoft.Testing.Platform.OutputDevice.ProgressReportingConfiguration
Microsoft.Testing.Platform.OutputDevice.SimplifiedConsoleOutputDeviceBase.ReportSlowTestsOnceAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Microsoft.Testing.Platform.OutputDevice.SimplifiedConsoleOutputDeviceBase.SimplifiedConsoleOutputDeviceBase(Microsoft.Testing.Platform.Helpers.IConsole! console, Microsoft.Testing.Platform.Services.ITestApplicationModuleInfo! testApplicationModuleInfo, Microsoft.Testing.Platform.Helpers.IAsyncMonitor! asyncMonitor, Microsoft.Testing.Platform.Helpers.IRuntimeFeature! runtimeFeature, Microsoft.Testing.Platform.Helpers.IEnvironment! environment, Microsoft.Testing.Platform.Services.IPlatformInformation! platformInformation, Microsoft.Testing.Platform.Services.IStopPoliciesService! policiesService, System.TimeSpan slowTestThreshold, System.Func<Microsoft.Testing.Platform.Helpers.IStopwatch!>! createStopwatch, System.TimeSpan slowTestPollInterval) -> void
Microsoft.Testing.Platform.OutputDevice.SlowTestDiagnostic
Microsoft.Testing.Platform.OutputDevice.SlowTestDiagnostic.DisplayName.get -> string!
Microsoft.Testing.Platform.OutputDevice.SlowTestDiagnostic.Elapsed.get -> System.TimeSpan
Microsoft.Testing.Platform.OutputDevice.SlowTestDiagnostic.Generation.get -> long
Microsoft.Testing.Platform.OutputDevice.SlowTestDiagnostic.SlowTestDiagnostic(Microsoft.Testing.Platform.Extensions.Messages.TestNodeUid! uid, string! displayName, System.TimeSpan elapsed, long generation) -> void
Microsoft.Testing.Platform.OutputDevice.SlowTestDiagnostic.Uid.get -> Microsoft.Testing.Platform.Extensions.Messages.TestNodeUid!
Microsoft.Testing.Platform.OutputDevice.SlowTestThresholdState
Microsoft.Testing.Platform.OutputDevice.SlowTestThresholdState.IsDue(System.TimeSpan elapsed) -> bool
Microsoft.Testing.Platform.OutputDevice.SlowTestThresholdState.SlowTestThresholdState(System.TimeSpan initialThreshold) -> void
static Microsoft.Testing.Platform.OutputDevice.ProgressReportingConfiguration.GetThreshold(Microsoft.Testing.Platform.Helpers.IEnvironment! environment, string! variableName, int defaultSeconds) -> System.TimeSpan
Microsoft.Testing.Platform.Extensions.Messages.TestNodeExecutionCompletedProperty
static Microsoft.Testing.Platform.Extensions.Messages.TestNodeExecutionCompletedProperty.CachedInstance.get -> Microsoft.Testing.Platform.Extensions.Messages.TestNodeExecutionCompletedProperty!
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ public override string ToString()
return builder.ToString();
}
}

internal sealed class TestNodeExecutionCompletedProperty : IProperty
{
private TestNodeExecutionCompletedProperty()
{
}

internal static TestNodeExecutionCompletedProperty CachedInstance { get; } = new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ This package provides the core platform and the .NET implementation of the proto
<InternalsVisibleTo Include="Microsoft.Testing.Extensions.UnitTests" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="Microsoft.Testing.Extensions.VSTestBridge" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="Microsoft.Testing.Extensions.VSTestBridge.UnitTests" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="Microsoft.Testing.Extensions.VideoRecorder" Key="$(VsPublicKey)" />
<!--
Microsoft.Testing.Framework / MSTest.Engine are the (now deprecated) source-generation engine assemblies.
They were renamed to Microsoft.Testing.Internal.Framework in https://github.com/microsoft/testfx/pull/2615,
Expand All @@ -79,6 +80,7 @@ This package provides the core platform and the .NET implementation of the proto
<InternalsVisibleTo Include="Microsoft.Testing.TestInfrastructure" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="MSTest.Engine" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="MSTest.TestAdapter" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="TestFramework.ForTestingMSTest" Key="$(VsPublicKey)" />
</ItemGroup>

Expand Down
Loading
Loading