diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/EventHandlers/TestRunEventsHandler.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/EventHandlers/TestRunEventsHandler.cs
index af4a23256c..8488323db5 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/EventHandlers/TestRunEventsHandler.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/EventHandlers/TestRunEventsHandler.cs
@@ -15,7 +15,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.EventHandle
///
/// The test run events handler.
///
-public class TestRunEventsHandler : IInternalTestRunEventsHandler
+public class TestRunEventsHandler : IInternalTestRunEventsHandler, ITestCaseLifecycleNotifier
{
private readonly ITestRequestHandler _requestHandler;
@@ -104,4 +104,12 @@ public bool AttachDebuggerToProcess(AttachDebuggerInfo attachDebuggerInfo)
EqtTrace.Info("Sending AttachDebuggerToProcess on additional test process with pid: {0}", attachDebuggerInfo.ProcessId);
return _requestHandler.AttachDebuggerToProcess(attachDebuggerInfo);
}
+
+ ///
+ void ITestCaseLifecycleNotifier.SendTestCaseStarting(TestCase testCase)
+ => _requestHandler.SendTestCaseStarting(testCase);
+
+ ///
+ void ITestCaseLifecycleNotifier.SendTestCaseFinished(TestCase testCase)
+ => _requestHandler.SendTestCaseFinished(testCase);
}
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/InFlightTestTracker.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/InFlightTestTracker.cs
new file mode 100644
index 0000000000..032d03995b
--- /dev/null
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/InFlightTestTracker.cs
@@ -0,0 +1,198 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel;
+
+namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
+
+///
+/// Tracks a set of in-flight test executions that share the same .
+/// Uses inline slots to avoid queue allocation in the common case (unique Guid per test).
+/// All mutation and enumeration is guarded by a lock because the message-receive thread
+/// updates slots while the abort thread may read them concurrently via .
+///
+internal sealed class InFlightTest
+{
+ private readonly object _lock = new();
+ public TestCaseStartingPayload Slot0;
+ public DateTimeOffset StartTime0;
+ public TestCaseStartingPayload? Slot1;
+ public DateTimeOffset StartTime1;
+ public TestCaseStartingPayload? Slot2;
+ public DateTimeOffset StartTime2;
+ public TestCaseStartingPayload? Slot3;
+ public DateTimeOffset StartTime3;
+ public Queue<(TestCaseStartingPayload Payload, DateTimeOffset StartTime)>? Overflow;
+
+ public InFlightTest(TestCaseStartingPayload payload, DateTimeOffset startTime)
+ {
+ Slot0 = payload;
+ StartTime0 = startTime;
+ }
+
+ ///
+ /// Adds another in-flight execution for the same Guid.
+ ///
+ public void Add(TestCaseStartingPayload payload, DateTimeOffset startTime)
+ {
+ lock (_lock)
+ {
+ if (Slot1 is null)
+ {
+ Slot1 = payload;
+ StartTime1 = startTime;
+ }
+ else if (Slot2 is null)
+ {
+ Slot2 = payload;
+ StartTime2 = startTime;
+ }
+ else if (Slot3 is null)
+ {
+ Slot3 = payload;
+ StartTime3 = startTime;
+ }
+ else
+ {
+ Overflow ??= new Queue<(TestCaseStartingPayload, DateTimeOffset)>();
+ Overflow.Enqueue((payload, startTime));
+ }
+ }
+ }
+
+ ///
+ /// Removes the oldest in-flight execution (FIFO). Returns true if there are still entries remaining.
+ ///
+ public bool RemoveOldest()
+ {
+ lock (_lock)
+ {
+ // Shift slots down
+ if (Slot1 is not null)
+ {
+ Slot0 = Slot1;
+ StartTime0 = StartTime1;
+ Slot1 = Slot2;
+ StartTime1 = StartTime2;
+ Slot2 = Slot3;
+ StartTime2 = StartTime3;
+ Slot3 = null;
+ StartTime3 = default;
+
+ // Refill Slot3 from overflow if available
+ if (Overflow is { Count: > 0 })
+ {
+ var (payload, startTime) = Overflow.Dequeue();
+ Slot3 = payload;
+ StartTime3 = startTime;
+ }
+
+ return true;
+ }
+
+ // Slot0 was the only entry
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a snapshot of all in-flight entries with their display name and start time.
+ ///
+ public IReadOnlyList<(string? DisplayName, DateTimeOffset StartTime)> GetAll()
+ {
+ lock (_lock)
+ {
+ var result = new List<(string?, DateTimeOffset)>();
+ result.Add((Slot0.DisplayName, StartTime0));
+
+ if (Slot1 is not null)
+ {
+ result.Add((Slot1.DisplayName, StartTime1));
+ }
+
+ if (Slot2 is not null)
+ {
+ result.Add((Slot2.DisplayName, StartTime2));
+ }
+
+ if (Slot3 is not null)
+ {
+ result.Add((Slot3.DisplayName, StartTime3));
+ }
+
+ if (Overflow is not null)
+ {
+ foreach (var (payload, startTime) in Overflow)
+ {
+ result.Add((payload.DisplayName, startTime));
+ }
+ }
+
+ return result;
+ }
+ }
+}
+
+///
+/// Thread-safe tracker for tests currently executing in a testhost.
+/// Used by vstest.console to report which tests were running when a testhost crashes.
+///
+internal sealed class InFlightTestTracker
+{
+ private readonly ConcurrentDictionary _tests = new();
+
+ ///
+ /// Records that a test has started executing.
+ ///
+ public void TestStarting(TestCaseStartingPayload payload, DateTimeOffset startTime)
+ {
+ _tests.AddOrUpdate(
+ payload.Id,
+ _ => new InFlightTest(payload, startTime),
+ (_, existing) =>
+ {
+ existing.Add(payload, startTime);
+ return existing;
+ });
+ }
+
+ ///
+ /// Records that a test has finished executing. Removes the oldest execution for the given Guid.
+ ///
+ public void TestFinished(Guid testId)
+ {
+ if (_tests.TryGetValue(testId, out var inFlight))
+ {
+ if (!inFlight.RemoveOldest())
+ {
+ _tests.TryRemove(testId, out _);
+ }
+ }
+ }
+
+ ///
+ /// Gets all tests that are currently in-flight. Used when testhost crashes to report what was running.
+ ///
+ public IReadOnlyList<(string? DisplayName, DateTimeOffset StartTime)> GetInFlightTests()
+ {
+ var result = new List<(string?, DateTimeOffset)>();
+ foreach (var kvp in _tests)
+ {
+ foreach (var entry in kvp.Value.GetAll())
+ {
+ result.Add(entry);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Returns true if there are any in-flight tests being tracked.
+ ///
+ public bool HasInFlightTests => !_tests.IsEmpty;
+}
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestCaseLifecycleNotifier.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestCaseLifecycleNotifier.cs
new file mode 100644
index 0000000000..f0ae730f14
--- /dev/null
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestCaseLifecycleNotifier.cs
@@ -0,0 +1,17 @@
+// 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.VisualStudio.TestPlatform.ObjectModel;
+
+namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces;
+
+///
+/// Provides lightweight test case lifecycle notifications for real-time in-flight tracking.
+/// Implemented by event handlers that can relay start/finish signals to the console.
+///
+internal interface ITestCaseLifecycleNotifier
+{
+ void SendTestCaseStarting(TestCase testCase);
+
+ void SendTestCaseFinished(TestCase testCase);
+}
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestRequestHandler.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestRequestHandler.cs
index f7f5ec0994..abb58a8774 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestRequestHandler.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestRequestHandler.cs
@@ -94,4 +94,18 @@ public interface ITestRequestHandler : IDisposable
/// Process ID and tfm of the process to which the debugger should be attached.
/// if the debugger was successfully attached to the requested process, otherwise.
bool AttachDebuggerToProcess(AttachDebuggerInfo attachDebuggerInfo);
+
+ ///
+ /// Sends a lightweight notification that a test case has started executing.
+ /// Used for real-time in-flight test tracking on the console side.
+ ///
+ /// The test case that started.
+ void SendTestCaseStarting(TestCase testCase);
+
+ ///
+ /// Sends a lightweight notification that a test case has finished executing.
+ /// Used for real-time in-flight test tracking on the console side.
+ ///
+ /// The test case that finished.
+ void SendTestCaseFinished(TestCase testCase);
}
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/MessageType.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/MessageType.cs
index 29381d0f07..674d3e6c16 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/MessageType.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/MessageType.cs
@@ -111,6 +111,18 @@ public static class MessageType
///
public const string TestRunStatsChange = "TestExecution.StatsChange";
+ ///
+ /// Lightweight notification that a test case has started executing.
+ /// Sent unbatched from testhost to console for real-time in-flight test tracking.
+ ///
+ public const string TestCaseStarting = "TestExecution.TestCaseStarting";
+
+ ///
+ /// Lightweight notification that a test case has finished executing.
+ /// Sent unbatched from testhost to console for real-time in-flight test tracking.
+ ///
+ public const string TestCaseFinished = "TestExecution.TestCaseFinished";
+
///
/// The execution complete.
///
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/TestCaseFinishedPayload.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/TestCaseFinishedPayload.cs
new file mode 100644
index 0000000000..3e4057ea02
--- /dev/null
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/TestCaseFinishedPayload.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel;
+
+///
+/// Payload for the TestCaseFinished message, sent from testhost to console when a test completes execution.
+///
+[DataContract]
+public class TestCaseFinishedPayload
+{
+ ///
+ /// Gets or sets the test case ID.
+ ///
+ [DataMember]
+ public Guid Id { get; set; }
+}
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/TestCaseStartingPayload.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/TestCaseStartingPayload.cs
new file mode 100644
index 0000000000..652b687d9c
--- /dev/null
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/TestCaseStartingPayload.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.Serialization;
+
+namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel;
+
+///
+/// Payload for the TestCaseStarting message, sent from testhost to console when a test begins execution.
+///
+[DataContract]
+public class TestCaseStartingPayload
+{
+ ///
+ /// Gets or sets the test case ID.
+ ///
+ [DataMember]
+ public Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the display name of the test case.
+ ///
+ [DataMember]
+ public string? DisplayName { get; set; }
+}
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/PublicAPI/PublicAPI.Unshipped.txt b/src/Microsoft.TestPlatform.CommunicationUtilities/PublicAPI/PublicAPI.Unshipped.txt
index 888f6721e5..2e90cee264 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/PublicAPI/PublicAPI.Unshipped.txt
@@ -1,5 +1,19 @@
#nullable enable
const Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.MessageType.TelemetryEventMessage = "TestPlatform.TelemetryEvent" -> string!
+const Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.MessageType.TestCaseFinished = "TestExecution.TestCaseFinished" -> string!
+const Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.MessageType.TestCaseStarting = "TestExecution.TestCaseStarting" -> string!
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ITestRequestHandler.SendTestCaseFinished(Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase! testCase) -> void
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces.ITestRequestHandler.SendTestCaseStarting(Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase! testCase) -> void
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseFinishedPayload
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseFinishedPayload.Id.get -> System.Guid
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseFinishedPayload.Id.set -> void
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseFinishedPayload.TestCaseFinishedPayload() -> void
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseStartingPayload
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseStartingPayload.DisplayName.get -> string?
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseStartingPayload.DisplayName.set -> void
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseStartingPayload.Id.get -> System.Guid
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseStartingPayload.Id.set -> void
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel.TestCaseStartingPayload.TestCaseStartingPayload() -> void
~static Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources.ConnectionTimeoutProcessDidNotStartErrorMessage.get -> string
~static Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources.ConnectionTimeoutProcessExitedErrorMessage.get -> string
~static Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources.ConnectionTimeoutWithDetailsErrorMessage.get -> string
diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs
index 89f7369dbd..fc6abc04c1 100644
--- a/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs
+++ b/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs
@@ -50,6 +50,7 @@ public class TestRequestSender : ITestRequestSender
// that implies host is using version 1.
private int _protocolVersion = 1;
private bool _isDiscoveryAborted;
+ private readonly InFlightTestTracker _inFlightTests = new();
///
/// Initializes a new instance of the class.
@@ -593,6 +594,24 @@ private void OnExecutionMessageReceived(MessageReceivedEventArgs messageReceived
_channel.Send(resultMessage);
break;
+
+ case MessageType.TestCaseStarting:
+ var testCaseStarting = _dataSerializer.DeserializePayload(message);
+ if (testCaseStarting is not null)
+ {
+ EqtTrace.Info("TestRequestSender.OnExecutionMessageReceived: TestCaseStarting received for: {0}", testCaseStarting.DisplayName);
+ _inFlightTests.TestStarting(testCaseStarting, DateTimeOffset.UtcNow);
+ }
+ break;
+
+ case MessageType.TestCaseFinished:
+ var testCaseFinished = _dataSerializer.DeserializePayload(message);
+ if (testCaseFinished is not null)
+ {
+ EqtTrace.Info("TestRequestSender.OnExecutionMessageReceived: TestCaseFinished received for: {0}", testCaseFinished.Id);
+ _inFlightTests.TestFinished(testCaseFinished.Id);
+ }
+ break;
}
}
catch (Exception exception)
@@ -748,6 +767,14 @@ private void OnDiscoveryAbort(ITestDiscoveryEventsHandler2 eventHandler, Excepti
// TODO: this timeout is 10 seconds, make it also configurable like the other famous timeout that is 100ms
if (_clientExited.Wait(_clientExitedWaitTime))
{
+ // Give a brief moment for any in-flight protocol messages (like TestCaseStarting)
+ // to be processed before we build the error. The message receive loop runs on a
+ // separate thread and may still be draining buffered messages.
+ if (!_inFlightTests.HasInFlightTests)
+ {
+ SpinWait.SpinUntil(() => _inFlightTests.HasInFlightTests, millisecondsTimeout: 50);
+ }
+
// Set a default message of test host process exited and additionally specify the error if we were able to get it
// from error output of the process
EqtTrace.Info("TestRequestSender.GetAbortErrorMessage: Received test host error message.");
@@ -757,15 +784,47 @@ private void OnDiscoveryAbort(ITestDiscoveryEventsHandler2 eventHandler, Excepti
reason = $"{reason} : {_clientExitErrorMessage}";
}
+ reason = AppendInFlightTestsInfo(reason);
+
return reason;
}
else
{
EqtTrace.Info("TestRequestSender.GetAbortErrorMessage: Timed out waiting for test host error message.");
- return CommonResources.UnableToCommunicateToTestHost;
+ return AppendInFlightTestsInfo(CommonResources.UnableToCommunicateToTestHost);
+ }
+ }
+
+ private string AppendInFlightTestsInfo(string reason)
+ {
+ var sb = new System.Text.StringBuilder(reason);
+
+ if (_inFlightTests.HasInFlightTests)
+ {
+ var inFlight = _inFlightTests.GetInFlightTests();
+ var now = DateTimeOffset.UtcNow;
+ sb.AppendLine();
+ sb.AppendLine("Tests that were executing when the crash occurred:");
+ foreach (var (displayName, startTime) in inFlight)
+ {
+ var elapsed = now - startTime;
+ sb.AppendLine($" {displayName ?? ""} (running for {FormatElapsed(elapsed)})");
+ }
}
+
+ sb.AppendLine();
+ sb.Append("Consider using --blame-crash to collect a crash dump for further diagnosis.");
+
+ return sb.ToString();
}
+ private static string FormatElapsed(TimeSpan elapsed) => elapsed.TotalSeconds switch
+ {
+ < 1 => $"{elapsed.Milliseconds}ms",
+ < 60 => $"{elapsed.TotalSeconds:F0}s",
+ _ => $"{(int)elapsed.TotalMinutes}m {elapsed.Seconds}s",
+ };
+
private void LogErrorMessage(string message)
{
if (_messageEventHandler == null)
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/FrameworkHandle.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/FrameworkHandle.cs
index 8d8ee0be6c..bddb65fec2 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/FrameworkHandle.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/FrameworkHandle.cs
@@ -45,9 +45,12 @@ internal class FrameworkHandle : TestExecutionRecorder, IFrameworkHandle2, IDisp
/// The test run cache.
/// The test execution context.
/// TestRun Events Handler
+ /// Optional callback for real-time in-flight test tracking.
+ /// Optional callback for real-time in-flight test tracking.
public FrameworkHandle(ITestCaseEventsHandler? testCaseEventsHandler, ITestRunCache testRunCache,
- TestExecutionContext testExecutionContext, IInternalTestRunEventsHandler testRunEventsHandler)
- : base(testCaseEventsHandler, testRunCache)
+ TestExecutionContext testExecutionContext, IInternalTestRunEventsHandler testRunEventsHandler,
+ Action? onTestCaseStarting = null, Action? onTestCaseFinished = null)
+ : base(testCaseEventsHandler, testRunCache, onTestCaseStarting, onTestCaseFinished)
{
_testExecutionContext = testExecutionContext;
_testRunEventsHandler = testRunEventsHandler;
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/TestExecutionRecorder.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/TestExecutionRecorder.cs
index 6ae681e5d5..8d557c8180 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/TestExecutionRecorder.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/TestExecutionRecorder.cs
@@ -21,6 +21,8 @@ internal class TestExecutionRecorder : TestSessionMessageLogger, ITestExecutionR
private readonly List _attachmentSets;
private readonly ITestRunCache _testRunCache;
private readonly ITestCaseEventsHandler? _testCaseEventsHandler;
+ private readonly Action? _onTestCaseStarting;
+ private readonly Action? _onTestCaseFinished;
///
/// Contains TestCase Ids for test cases that are in progress
@@ -35,10 +37,18 @@ internal class TestExecutionRecorder : TestSessionMessageLogger, ITestExecutionR
///
/// The test Case Events Handler.
/// The test run cache.
- public TestExecutionRecorder(ITestCaseEventsHandler? testCaseEventsHandler, ITestRunCache testRunCache)
+ /// Optional callback invoked when a test case starts, for real-time in-flight tracking.
+ /// Optional callback invoked when a test case finishes, for real-time in-flight tracking.
+ public TestExecutionRecorder(
+ ITestCaseEventsHandler? testCaseEventsHandler,
+ ITestRunCache testRunCache,
+ Action? onTestCaseStarting = null,
+ Action? onTestCaseFinished = null)
{
_testRunCache = testRunCache;
_testCaseEventsHandler = testCaseEventsHandler;
+ _onTestCaseStarting = onTestCaseStarting;
+ _onTestCaseFinished = onTestCaseFinished;
_attachmentSets = new List();
// As a framework guideline, we should get events in this order:
@@ -70,6 +80,7 @@ public void RecordStart(TestCase testCase)
{
EqtTrace.Verbose("TestExecutionRecorder.RecordStart: Starting test: {0}.", testCase.FullyQualifiedName);
_testRunCache.OnTestStarted(testCase);
+ _onTestCaseStarting?.Invoke(testCase);
if (_testCaseEventsHandler != null)
{
@@ -115,6 +126,7 @@ public void RecordEnd(TestCase testCase, TestOutcome outcome)
{
EqtTrace.Verbose("TestExecutionRecorder.RecordEnd: test: {0} execution completed.", testCase.FullyQualifiedName);
_testRunCache.OnTestCompletion(testCase);
+ _onTestCaseFinished?.Invoke(testCase);
SendTestCaseEnd(testCase, outcome);
}
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/EventHandlers/TestRequestHandler.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/EventHandlers/TestRequestHandler.cs
index 0c9fb56ed2..6747be63b2 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/EventHandlers/TestRequestHandler.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/EventHandlers/TestRequestHandler.cs
@@ -613,4 +613,21 @@ private void SendData(string data)
EqtTrace.Verbose("TestRequestHandler.SendData: sending data from testhost: {0}", data);
_channel?.Send(data);
}
+
+ ///
+ public void SendTestCaseStarting(TestCase testCase)
+ {
+ EqtTrace.Info("TestRequestHandler.SendTestCaseStarting: Sending test case starting for: {0}", testCase.DisplayName);
+ var payload = new TestCaseStartingPayload { Id = testCase.Id, DisplayName = testCase.DisplayName };
+ var data = _dataSerializer.SerializePayload(MessageType.TestCaseStarting, payload, _protocolVersion);
+ SendData(data);
+ }
+
+ ///
+ public void SendTestCaseFinished(TestCase testCase)
+ {
+ var payload = new TestCaseFinishedPayload { Id = testCase.Id };
+ var data = _dataSerializer.SerializePayload(MessageType.TestCaseFinished, payload, _protocolVersion);
+ SendData(data);
+ }
}
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs
index 2f9c975b7f..9defd0aac2 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs
@@ -156,11 +156,14 @@ protected BaseRunTests(
RunContext.SolutionDirectory = RunSettingsUtilities.GetSolutionDirectory(runConfig);
_runConfiguration = runConfig;
+ var notifier = testRunEventsHandler as ITestCaseLifecycleNotifier;
FrameworkHandle = new FrameworkHandle(
_testCaseEventsHandler,
TestRunCache,
TestExecutionContext,
- TestRunEventsHandler);
+ TestRunEventsHandler,
+ onTestCaseStarting: notifier is not null ? tc => notifier.SendTestCaseStarting(tc) : null,
+ onTestCaseFinished: notifier is not null ? tc => notifier.SendTestCaseFinished(tc) : null);
FrameworkHandle.TestRunMessage += OnTestRunMessage;
ExecutorUrisThatRanTests = new List();
diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/PublicAPI/PublicAPI.Unshipped.txt b/src/Microsoft.TestPlatform.CrossPlatEngine/PublicAPI/PublicAPI.Unshipped.txt
index 7dc5c58110..6190d4a803 100644
--- a/src/Microsoft.TestPlatform.CrossPlatEngine/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.TestPlatform.CrossPlatEngine/PublicAPI/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.TestRequestHandler.SendTestCaseFinished(Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase! testCase) -> void
+Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.TestRequestHandler.SendTestCaseStarting(Microsoft.VisualStudio.TestPlatform.ObjectModel.TestCase! testCase) -> void
diff --git a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/ExecutionTests.cs b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/ExecutionTests.cs
index f19d76ad79..56285bc08d 100644
--- a/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/ExecutionTests.cs
+++ b/test/Microsoft.TestPlatform.Acceptance.IntegrationTests/ExecutionTests.cs
@@ -228,6 +228,59 @@ public void UnhandleExceptionExceptionShouldBeLoggedToDiagLogFile(RunnerInfo run
File.Delete(diagLogFilePath);
}
+ [TestMethod]
+ [NetCoreTargetFrameworkDataSource]
+ public void WhenTestHostCrashesTheErrorSuggestsUsingBlameCrash(RunnerInfo runnerInfo)
+ {
+ SetTestEnvironment(_testEnvironment, runnerInfo);
+
+ var assemblyPaths = GetAssetFullPath("SimpleTestProject3.dll");
+ var arguments = PrepareArguments(assemblyPaths, GetTestAdapterPath(), string.Empty, FrameworkArgValue, runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
+ arguments = string.Concat(arguments, " /testcasefilter:ExitwithUnhandleException");
+
+ InvokeVsTest(arguments);
+
+ ExitCodeEquals(1);
+ // The error message should suggest using --blame-crash for further diagnosis.
+ StdErrorContains("--blame-crash");
+ }
+
+ [TestMethod]
+ [NetCoreTargetFrameworkDataSource]
+ public void WhenTestHostCrashesWithUnhandledExceptionTheErrorShowsInFlightTest(RunnerInfo runnerInfo)
+ {
+ SetTestEnvironment(_testEnvironment, runnerInfo);
+
+ var assemblyPaths = GetAssetFullPath("SimpleTestProject3.dll");
+ var arguments = PrepareArguments(assemblyPaths, GetTestAdapterPath(), string.Empty, FrameworkArgValue, runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
+ arguments = string.Concat(arguments, " /testcasefilter:ExitwithUnhandleException");
+
+ InvokeVsTest(arguments);
+
+ ExitCodeEquals(1);
+ StdErrorContains("Tests that were executing when the crash occurred:");
+ StdErrorContains("ExitwithUnhandleException");
+ }
+
+ [TestMethod]
+ [NetCoreTargetFrameworkDataSource]
+ public void WhenTestHostCrashesWithStackOverflowTheErrorShowsInFlightTest(RunnerInfo runnerInfo)
+ {
+ SetTestEnvironment(_testEnvironment, runnerInfo);
+
+ var assemblyPaths = GetAssetFullPath("SimpleTestProject3.dll");
+ var arguments = PrepareArguments(assemblyPaths, GetTestAdapterPath(), string.Empty, FrameworkArgValue, runnerInfo.InIsolationValue, resultsDirectory: TempDirectory.Path);
+ arguments = string.Concat(arguments, " /testcasefilter:ExitWithStackoverFlow");
+
+ InvokeVsTest(arguments);
+
+ ExitCodeEquals(1);
+ // Even for stack overflow, the error should suggest --blame-crash.
+ StdErrorContains("--blame-crash");
+ // The test name should appear if the message was sent before the crash.
+ // Stack overflow may kill the process before the message arrives, so we only check blame-crash suggestion.
+ }
+
[TestMethod]
[TestCategory("Windows-Review")]
[NetFullTargetFrameworkDataSource]
diff --git a/test/Microsoft.TestPlatform.CommunicationUtilities.UnitTests/TestRequestSenderTests.cs b/test/Microsoft.TestPlatform.CommunicationUtilities.UnitTests/TestRequestSenderTests.cs
index f1df84a12d..18b4ce8f4c 100644
--- a/test/Microsoft.TestPlatform.CommunicationUtilities.UnitTests/TestRequestSenderTests.cs
+++ b/test/Microsoft.TestPlatform.CommunicationUtilities.UnitTests/TestRequestSenderTests.cs
@@ -212,7 +212,7 @@ public void OnClientProcessExitShouldSendErrorMessageIfStdErrIsEmpty(string stde
var expectedErrorMessage = "Reason: Test host process crashed";
RaiseClientDisconnectedEvent();
- _mockDiscoveryEventsHandler.Verify(eh => eh.HandleLogMessage(TestMessageLevel.Error, It.Is(s => s.EndsWith(expectedErrorMessage))), Times.Once);
+ _mockDiscoveryEventsHandler.Verify(eh => eh.HandleLogMessage(TestMessageLevel.Error, It.Is(s => s.Contains(expectedErrorMessage))), Times.Once);
}
[TestMethod]