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
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
Expand All @@ -17,51 +18,26 @@ internal void SendTestResults(
TestTools.UnitTesting.TestResult[] unitTestResults,
DateTimeOffset startTime,
DateTimeOffset endTime,
ITestExecutionRecorder testExecutionRecorder)
ITestResultRecorder testResultRecorder)
{
if (unitTestResults.Length == 0)
{
testExecutionRecorder.RecordEnd(test, TestOutcome.None);
testResultRecorder.RecordEmptyResult(test);
return;
}

foreach (TestTools.UnitTesting.TestResult unitTestResult in unitTestResults)
{
_testRunCancellationToken?.ThrowIfCancellationRequested();

var testResult = unitTestResult.ToTestResult(
test,
startTime,
endTime,
_environment.MachineName,
MSTestSettings.CurrentSettings);

testExecutionRecorder.RecordEnd(test, testResult.Outcome);

if (testResult.Outcome == TestOutcome.Failed)
{
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Info("MSTestExecutor:Test {0} failed. ErrorMessage:{1}, ErrorStackTrace:{2}.", testResult.TestCase.FullyQualifiedName, testResult.ErrorMessage, testResult.ErrorStackTrace);
}

#if !WINDOWS_UWP && !WIN_UI
_hasAnyTestFailed = true;
#endif
}

try
{
if (testResult.Outcome != TestOutcome.NotFound
|| !RuntimeContext.IsHotReloadEnabled)
{
testExecutionRecorder.RecordResult(testResult);
}
}
catch (TestCanceledException)
if (testResultRecorder.RecordResult(test, unitTestResult, startTime, endTime))
{
// Ignore this exception
_hasAnyTestFailed = true;
}
#else
testResultRecorder.RecordResult(test, unitTestResult, startTime, endTime);
#endif
}
}

Expand Down Expand Up @@ -95,6 +71,10 @@ private async Task ExecuteTestsWithTestRunnerAsync(
? new RemotingMessageLogger(testExecutionRecorder)
: testExecutionRecorder;

// Translate the VSTest recorder into the platform-agnostic result recorder a single time for this
// test set. This is the boundary at which VSTest result construction is applied.
ITestResultRecorder testResultRecorder = testExecutionRecorder.ToTestResultRecorder(_environment.MachineName, MSTestSettings.CurrentSettings);

foreach (TestCase currentTest in orderedTests)
{
_testRunCancellationToken?.ThrowIfCancellationRequested();
Expand All @@ -105,7 +85,7 @@ private async Task ExecuteTestsWithTestRunnerAsync(

UnitTestElement unitTestElement = currentTest.ToUnitTestElementWithUpdatedSource(source);

testExecutionRecorder.RecordStart(currentTest);
testResultRecorder.RecordStart(currentTest);

DateTimeOffset startTime = DateTimeOffset.Now;

Expand Down Expand Up @@ -143,7 +123,7 @@ private async Task ExecuteTestsWithTestRunnerAsync(

DateTimeOffset endTime = DateTimeOffset.Now;

SendTestResults(currentTest, unitTestResult, startTime, endTime, testExecutionRecorder);
SendTestResults(currentTest, unitTestResult, startTime, endTime, testResultRecorder);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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;

using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult;

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;

/// <summary>
/// Platform-agnostic sink for reporting the lifecycle and results of test execution to whichever
/// test host is running the tests.
/// </summary>
/// <remarks>
/// This abstraction lets the platform services layer report test start/end and results without taking a
/// dependency on a specific test platform's result object model (for example the VSTest <c>TestResult</c>,
/// <c>TestOutcome</c> and attachment types). The concrete recorder is provided at the platform boundary by a
/// wrapper over the host's result recorder (currently <c>TestResultRecorderExtensions</c>, which wraps the
/// VSTest <c>ITestExecutionRecorder</c>), and is expected to move fully out of the platform services layer in
/// a later phase.
/// </remarks>
internal interface ITestResultRecorder
{
/// <summary>
/// Signals that execution of the given test case has started.
/// </summary>
/// <param name="testCase">The test case whose execution is starting.</param>
void RecordStart(TestCase testCase);

/// <summary>
/// Signals that execution of the given test case ended without producing any result.
/// </summary>
/// <param name="testCase">The test case whose execution ended.</param>
void RecordEmptyResult(TestCase testCase);

/// <summary>
/// Reports a single framework <see cref="FrameworkTestResult"/> for the given test case to the test host.
/// </summary>
/// <param name="testCase">The test case the result belongs to.</param>
/// <param name="unitTestResult">The framework result produced by executing the test.</param>
/// <param name="startTime">The time at which the test started executing.</param>
/// <param name="endTime">The time at which the test finished executing.</param>
/// <returns><see langword="true"/> if the result was reported as a failure; otherwise, <see langword="false"/>.</returns>
bool RecordResult(TestCase testCase, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;

using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult;

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;

/// <summary>
/// Bridges a VSTest <see cref="ITestExecutionRecorder"/> to the platform-agnostic <see cref="ITestResultRecorder"/>.
/// </summary>
/// <remarks>
/// This is the single translation point between the VSTest result object model (<c>TestResult</c>,
/// <c>TestOutcome</c>, attachments, ...) and the platform services result-reporting abstraction. It is
/// expected to move entirely into the adapter layer once the execution pipeline no longer flows VSTest
/// recorders through the platform services.
/// </remarks>
internal static class TestResultRecorderExtensions
{
/// <summary>
/// Wraps a VSTest <see cref="ITestExecutionRecorder"/> as an <see cref="ITestResultRecorder"/>.
/// </summary>
/// <param name="testExecutionRecorder">The host recorder to wrap.</param>
/// <param name="computerName">The computer name stamped on reported results.</param>
/// <param name="settings">The current MSTest settings used to map framework outcomes to host outcomes.</param>
/// <returns>A platform-agnostic recorder that forwards to <paramref name="testExecutionRecorder"/>.</returns>
public static ITestResultRecorder ToTestResultRecorder(this ITestExecutionRecorder testExecutionRecorder, string computerName, MSTestSettings settings)
=> new HostTestResultRecorder(testExecutionRecorder, computerName, settings);

private sealed class HostTestResultRecorder : ITestResultRecorder
{
private readonly ITestExecutionRecorder _testExecutionRecorder;
private readonly string _computerName;
private readonly MSTestSettings _settings;

public HostTestResultRecorder(ITestExecutionRecorder testExecutionRecorder, string computerName, MSTestSettings settings)
{
_testExecutionRecorder = testExecutionRecorder;
_computerName = computerName;
_settings = settings;
}

public void RecordStart(TestCase testCase)
=> _testExecutionRecorder.RecordStart(testCase);

public void RecordEmptyResult(TestCase testCase)
=> _testExecutionRecorder.RecordEnd(testCase, TestOutcome.None);

public bool RecordResult(TestCase testCase, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime)
{
var testResult = unitTestResult.ToTestResult(testCase, startTime, endTime, _computerName, _settings);

_testExecutionRecorder.RecordEnd(testCase, testResult.Outcome);

bool isFailed = testResult.Outcome == TestOutcome.Failed;
if (isFailed && PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
{
PlatformServiceProvider.Instance.AdapterTraceLogger.Info("MSTestExecutor:Test {0} failed. ErrorMessage:{1}, ErrorStackTrace:{2}.", testResult.TestCase.FullyQualifiedName, testResult.ErrorMessage, testResult.ErrorStackTrace);
}

try
{
if (testResult.Outcome != TestOutcome.NotFound
|| !RuntimeContext.IsHotReloadEnabled)
{
_testExecutionRecorder.RecordResult(testResult);
}
}
catch (TestCanceledException)
{
// Ignore this exception
}

// A failure is reported only once RecordResult has completed (a swallowed TestCanceledException
// still counts as completed). This mirrors the original inline flow where the failure was
// observed as part of reporting the result.
return isFailed;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public void SendTestResults_WhenUnitTestResultsIsEmpty_RecordsEndWithoutResult()
TestCase testCase = GetTestCase(typeof(DummyTestClass), "PassingTest");
Microsoft.VisualStudio.TestTools.UnitTesting.TestResult[] unitTestResults = [];

_testExecutionManager.SendTestResults(testCase, unitTestResults, DateTimeOffset.Now, DateTimeOffset.Now, _frameworkHandle);
_testExecutionManager.SendTestResults(testCase, unitTestResults, DateTimeOffset.Now, DateTimeOffset.Now, _frameworkHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings));

_frameworkHandle.TestCaseEndList.Should().Equal("PassingTest:None");
_frameworkHandle.ResultsList.Should().BeEmpty();
Expand Down
Loading