Skip to content
Open
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 @@ -4,6 +4,7 @@
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.Resources;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using VSTestAttachmentSet = Microsoft.VisualStudio.TestPlatform.ObjectModel.AttachmentSet;
Expand Down
1 change: 1 addition & 0 deletions src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
<ItemGroup>
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" Key="$(MoqPublicKey)" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="MSTestAdapter.PlatformServices.UnitTests" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="MSTest.VstestConsoleWrapper.IntegrationTests" Key="$(VsPublicKey)" />
<InternalsVisibleTo Include="MSTest.IntegrationTests" Key="$(VsPublicKey)" />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
// 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.MSTest.TestAdapter.ObjectModel;
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;
namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;

/// <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.
/// This is the single translation point between the neutral execution model (<see cref="UnitTestElement"/> and
/// the framework <see cref="FrameworkTestResult"/>) and the VSTest result object model (<c>TestResult</c>,
/// <c>TestOutcome</c>, attachments, ...). It lives in the adapter layer; the platform services engine reports
/// through the neutral <see cref="ITestResultRecorder"/> only. The element's host test case is resolved (and
/// materialized/cached on demand for internally discovered tests) via <see cref="UnitTestElement.GetOrCreateHostTestCase"/>
/// so recorded results preserve host-injected data (test-case-management / data-collector properties) with full fidelity.
/// </remarks>
internal static class TestResultRecorderExtensions
{
Expand All @@ -46,14 +48,15 @@ public HostTestResultRecorder(ITestExecutionRecorder testExecutionRecorder, stri
_settings = settings;
}

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

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

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

_testExecutionRecorder.RecordEnd(testCase, testResult.Outcome);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,20 @@ public bool Matches(UnitTestElement testElement)
}
}
}

/// <summary>
/// Adapter-boundary <see cref="ITestElementFilterProvider"/> that closes over the VSTest discovery/run context
/// and produces the neutral <see cref="ITestElementFilter"/> via <see cref="TestMethodFilter"/>. Created at the
/// boundary and passed into the platform services engine/discoverer so the filter (and any parse-error report)
/// is built at the exact point the engine needs it, preserving the original per-source timing.
/// </summary>
internal sealed class TestElementFilterProvider : ITestElementFilterProvider
{
private readonly TestMethodFilter _testMethodFilter = new();
private readonly IDiscoveryContext? _context;

public TestElementFilterProvider(IDiscoveryContext? context) => _context = context;

public ITestElementFilter? GetTestElementFilter(IAdapterMessageLogger logger, out bool filterHasError)
=> _testMethodFilter.GetTestElementFilter(_context, logger, out filterHasError);
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ internal async Task DiscoverTestsAsync(IEnumerable<string> sources, IDiscoveryCo
try
{
IAdapterMessageLogger adapterLogger = logger.ToAdapterMessageLogger();
if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, adapterLogger, configuration, _testSourceHandler))
if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext?.RunSettings?.SettingsXml, adapterLogger, configuration, _testSourceHandler))
{
new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink.ToUnitTestElementSink(), discoveryContext, isMTP);
new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink.ToUnitTestElementSink(), discoveryContext?.RunSettings?.SettingsXml, new TestElementFilterProvider(discoveryContext), isMTP);
}
}
finally
Expand Down
42 changes: 38 additions & 4 deletions src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.VSTestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources;
Expand Down Expand Up @@ -139,19 +142,41 @@ internal async Task RunTestsAsync(IEnumerable<TestCase>? tests, IRunContext? run

try
{
if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext, frameworkHandle.ToAdapterMessageLogger(), configuration, new TestSourceHandler()))
if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger(), configuration, new TestSourceHandler()))
{
return;
}

await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(tests, runContext, frameworkHandle, testRunToken).ConfigureAwait(false)).ConfigureAwait(false);
// Convert the host test cases into the neutral model at this boundary so the execution engine runs
// on UnitTestElement end-to-end. Each element keeps its originating test case as an opaque handle
// (for full-fidelity result recording and deployment) plus the host execution-context (TCM)
// properties surfaced through TestContext.
UnitTestElement[] testElements = [.. tests.Select(ToUnitTestElement)];

// Translate the VSTest recorder into the platform-agnostic result recorder at this boundary; the
// execution engine reports results through this neutral recorder and never constructs VSTest results.
ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings);

// Extract the neutral run inputs (test-run directory + run settings XML) from the host run context at
// this boundary so the execution engine no longer depends on the VSTest run context.
var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml);

await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, deploymentContext, frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), testRunToken).ConfigureAwait(false)).ConfigureAwait(false);
}
finally
{
await SendTelemetryAsync().ConfigureAwait(false);
}
}

private static UnitTestElement ToUnitTestElement(TestCase testCase)
{
UnitTestElement testElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source);
testElement.ExecutionContextProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase);
testElement.HostRecordingHandle = testCase;
return testElement;
}

internal async Task RunTestsAsync(IEnumerable<string>? sources, IRunContext? runContext, IFrameworkHandle? frameworkHandle, IConfiguration? configuration, bool isMTP)
{
if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
Expand Down Expand Up @@ -183,13 +208,22 @@ internal async Task RunTestsAsync(IEnumerable<string>? sources, IRunContext? run
try
{
TestSourceHandler testSourceHandler = new();
if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext, frameworkHandle.ToAdapterMessageLogger(), configuration, testSourceHandler))
if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger(), configuration, testSourceHandler))
{
return;
}

sources = testSourceHandler.GetTestSources(sources);
await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false);

// Translate the VSTest recorder into the platform-agnostic result recorder at this boundary; the
// execution engine reports results through this neutral recorder and never constructs VSTest results.
ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings);

// Extract the neutral run inputs (test-run directory + run settings XML) from the host run context at
// this boundary so the execution engine no longer depends on the VSTest run context.
var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml);

await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, deploymentContext, frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false);
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

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

/// <summary>
/// Platform-agnostic inputs the execution pipeline needs from the running test host: the test-run/results
/// directory and the run settings XML. This lets the platform services execution and deployment layers run
/// without taking a dependency on a specific test platform's run-context object model (for example the VSTest
/// <c>IRunContext</c> / <c>IRunSettings</c> types). It is populated at the adapter boundary.
/// </summary>
internal sealed class DeploymentContext
{
public DeploymentContext(string? testRunDirectory, string? runSettingsXml)
{
TestRunDirectory = testRunDirectory;
RunSettingsXml = runSettingsXml;
}

/// <summary>
/// Gets the host-provided test-run directory (the root under which results/deployment directories are
/// created), or <see langword="null"/> to fall back to a temp directory.
/// </summary>
public string? TestRunDirectory { get; }

/// <summary>
/// Gets the run settings XML supplied by the host, or <see langword="null"/> when none was provided.
/// </summary>
public string? RunSettingsXml { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
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.Adapter;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery;
Expand All @@ -23,7 +22,7 @@ internal sealed class AssemblyEnumeratorWrapper
/// Gets test elements from an assembly.
/// </summary>
/// <returns> A collection of test elements. </returns>
internal static ICollection<UnitTestElement>? GetTests(string? assemblyFileName, IRunSettings? runSettings, ITestSourceHandler testSourceHandler, bool isMTP, out List<string> warnings)
internal static ICollection<UnitTestElement>? GetTests(string? assemblyFileName, string? settingsXml, ITestSourceHandler testSourceHandler, bool isMTP, out List<string> warnings)
{
warnings = [];

Expand All @@ -48,7 +47,7 @@ internal sealed class AssemblyEnumeratorWrapper
try
{
// Load the assembly in isolation if required.
AssemblyEnumerationResult result = GetTestsInIsolation(fullFilePath, runSettings, isMTP);
AssemblyEnumerationResult result = GetTestsInIsolation(fullFilePath, settingsXml, isMTP);
warnings.AddRange(result.Warnings);
return result.TestElements;
}
Expand Down Expand Up @@ -86,9 +85,9 @@ internal sealed class AssemblyEnumeratorWrapper
}
}

private static AssemblyEnumerationResult GetTestsInIsolation(string fullFilePath, IRunSettings? runSettings, bool isMTP)
private static AssemblyEnumerationResult GetTestsInIsolation(string fullFilePath, string? settingsXml, bool isMTP)
{
using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(fullFilePath, runSettings);
using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(fullFilePath, settingsXml);

// Create an instance of a type defined in adapter so that adapter gets loaded in the child app domain
var assemblyEnumerator = (AssemblyEnumerator)isolationHost.CreateInstanceForType(typeof(AssemblyEnumerator), [MSTestSettings.CurrentSettings])!;
Expand Down
Loading
Loading