Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testhost sharing between discovery & execution #2687

Merged
merged 32 commits into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cad90d0
Groundwork for testhost sharing
cvpoienaru Jan 11, 2021
859965e
Enabled test session info passing through TP
cvpoienaru Jan 11, 2021
30775f1
Testhost sharing is working
cvpoienaru Jan 12, 2021
b9057db
Fixed double channel close
cvpoienaru Jan 14, 2021
bd267b7
Fixed not logging test platform event source when request is successful
cvpoienaru Jan 15, 2021
03c5c96
Merge branch 'master' into copoiena/tp-testhost-sharing
cvpoienaru Jan 15, 2021
80059ae
Added logic to avoid testhost cleanup
cvpoienaru Jan 18, 2021
809d6f4
Using console wrapper instead of test session
cvpoienaru Jan 19, 2021
b831d87
Reverted to previous interface
cvpoienaru Jan 22, 2021
8ebaabc
Removed trailing whitespaces after restoring previous interface
cvpoienaru Jan 25, 2021
ff8507b
Reverted whitespaces
cvpoienaru Jan 25, 2021
629f20f
Reverted whitespaces 2
cvpoienaru Jan 25, 2021
f91228e
Added check for matching run settings and disabled test sessions with…
cvpoienaru Jan 25, 2021
3c2e26b
Refactored SetupChannel
cvpoienaru Jan 25, 2021
ef039a2
Implemented a disposable TestSession
cvpoienaru Jan 27, 2021
38df898
Merge branch 'master' into copoiena/tp-testhost-sharing
cvpoienaru Jan 29, 2021
55118ac
Merge branch 'main' into copoiena/tp-testhost-sharing
cvpoienaru Aug 31, 2021
3e43177
Checked out loc changes from main
cvpoienaru Aug 31, 2021
b3953b0
Adjusted the number of testhosts to be spawned
cvpoienaru Sep 8, 2021
58fd909
Fixed test container to test host association
cvpoienaru Sep 16, 2021
0c533f5
Merge branch 'main' into copoiena/tp-testhost-sharing
cvpoienaru Sep 16, 2021
90170c5
Fixed public API errors
cvpoienaru Sep 16, 2021
6328248
Fixed unit tests
cvpoienaru Sep 16, 2021
73fe5d0
Added more unit tests
cvpoienaru Oct 20, 2021
7a89d9a
Merge branch 'main' into copoiena/tp-testhost-sharing
cvpoienaru Oct 20, 2021
9cfd6bf
Fixed public API
cvpoienaru Oct 20, 2021
4b246a8
Added more tests
cvpoienaru Oct 25, 2021
a64fd37
Added more tests
cvpoienaru Oct 25, 2021
b80b578
Added Obsolete attributes to mark the API isn't final
cvpoienaru Nov 4, 2021
02d6205
Fixed code review comments
cvpoienaru Nov 12, 2021
6e75cfc
Merge branch 'main' into copoiena/tp-testhost-sharing
cvpoienaru Nov 12, 2021
393fb3e
Fixed review comments
cvpoienaru Nov 18, 2021
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
35 changes: 7 additions & 28 deletions src/Microsoft.TestPlatform.Client/TestPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public ITestRunRequest CreateTestRunRequest(
}

/// <inheritdoc/>
public void StartTestSession(
public bool StartTestSession(
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
IRequestData requestData,
StartTestSessionCriteria testSessionCriteria,
ITestSessionEventsHandler eventsHandler)
Expand All @@ -170,44 +170,23 @@ public void StartTestSession(
this.AddExtensionAssemblies(testSessionCriteria.RunSettings);

var runConfiguration = XmlRunSettingsUtilities.GetRunConfigurationNode(testSessionCriteria.RunSettings);

// Update extension assemblies from source when design mode is false.
//
// TODO (copoiena): Is it possible for this code to run if we're not in design mode ?
// An use case for this would be when running tests with "dotnet test". Usually there's
// a build involved then.
if (!runConfiguration.DesignMode)
{
return;
}

// Initialize loggers.
var loggerManager = this.TestEngine.GetLoggerManager(requestData);
loggerManager.Initialize(testSessionCriteria.RunSettings);

var testHostManager = this.testHostProviderManager.GetTestHostManagerByRunConfiguration(testSessionCriteria.RunSettings);
ThrowExceptionIfTestHostManagerIsNull(testHostManager, testSessionCriteria.RunSettings);

testHostManager.Initialize(TestSessionMessageLogger.Instance, testSessionCriteria.RunSettings);

if (testSessionCriteria.TestHostLauncher != null)
{
testHostManager.SetCustomLauncher(testSessionCriteria.TestHostLauncher);
return false;
}

var testSessionManager = this.TestEngine.GetTestSessionManager(requestData, testHostManager, testSessionCriteria);
var testSessionManager = this.TestEngine.GetTestSessionManager(requestData, testSessionCriteria);
if (testSessionManager == null)
{
// The test session manager is null because the combination of runsettings and
// sources tells us we should run in-process (i.e. in vstest.console). Because
// of this no session will be created because there's no testhost to be launched.
// Expecting a subsequent call to execute tests with the same set of parameters.
eventsHandler.HandleStartTestSessionComplete(null);
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
return;
return false;
}

testSessionManager.Initialize(false);
testSessionManager.StartSession(testSessionCriteria, eventsHandler);
return testSessionManager.StartSession(eventsHandler);
}

/// <summary>
Expand All @@ -234,11 +213,11 @@ public void ClearExtensions()

private void ThrowExceptionIfTestHostManagerIsNull(
ITestRuntimeProvider testHostManager,
string settingXml)
string settingsXml)
{
if (testHostManager == null)
{
EqtTrace.Error("TestPlatform.CreateTestRunRequest: No suitable testHostProvider found for runsettings : {0}", settingXml);
EqtTrace.Error("TestPlatform.CreateTestRunRequest: No suitable testHostProvider found for runsettings : {0}", settingsXml);
throw new TestPlatformException(string.Format(CultureInfo.CurrentCulture, ClientResources.NoTestHostProviderFound));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,22 @@ namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine
/// </summary>
public interface IProxyTestSessionManager
{
/// <summary>
/// Initialize the proxy.
/// </summary>
///
/// <param name="skipDefaultAdapters">Skip default adapters flag.</param>
void Initialize(bool skipDefaultAdapters);

/// <summary>
/// Starts the test session based on the test session criteria.
/// </summary>
///
/// <param name="criteria">The test session criteria.</param>
/// <param name="eventsHandler">
/// Event handler for handling events fired during test session management operations.
/// </param>
void StartSession(
StartTestSessionCriteria criteria,
ITestSessionEventsHandler eventsHandler);
///
/// <returns>True if the operation succeeded, false otherwise.</returns>
bool StartSession(ITestSessionEventsHandler eventsHandler);

/// <summary>
/// Stops the test session.
/// </summary>
void StopSession();
///
/// <returns>True if the operation succeeded, false otherwise.</returns>
bool StopSession();
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,13 @@ IProxyExecutionManager GetExecutionManager(
/// <param name="requestData">
/// The request data for providing test session services and data.
/// </param>
/// <param name="testHostManager">Test host manager for the current test session.</param>
/// <param name="testSessionCriteria">
/// Test session criteria of the current test session.
/// </param>
///
/// <returns>An IProxyTestSessionManager object that can manage test sessions.</returns>
IProxyTestSessionManager GetTestSessionManager(
IRequestData requestData,
ITestRuntimeProvider testHostManager,
StartTestSessionCriteria testSessionCriteria);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ internal TestRequestSender(
{
}

public bool CloseConnectionOnOperationComplete { get; set; } = true;

/// <inheritdoc />
public int InitializeCommunication()
{
Expand Down Expand Up @@ -727,6 +729,16 @@ private bool IsOperationComplete()

private void SetOperationComplete()
{
// When sharing the testhost between discovery and execution we must keep the
// testhost alive after completing the operation it was spawned for. As such we
// suppress the test request sender channel close taking place here. This channel
// will be closed when the test session owner decides to dispose of the test session
// object.
if (!this.CloseConnectionOnOperationComplete)
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
{
return;
}

// Complete the currently ongoing operation (Discovery/Execution)
if (EqtTrace.IsVerboseEnabled)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

using Microsoft.VisualStudio.TestPlatform.Common;
using Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework;
Expand All @@ -25,17 +26,65 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client
/// </summary>
public class ProxyDiscoveryManager : IProxyDiscoveryManager, IBaseProxy, ITestDiscoveryEventsHandler2
{
private ProxyOperationManager proxyOperationManager;
private readonly ITestRuntimeProvider testHostManager;
private IDataSerializer dataSerializer;
private bool isCommunicationEstablished;
private readonly TestSessionInfo testSessionInfo = null;
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
private readonly string runSettings;
private readonly IRequestData backupRequestData;
private readonly ITestRequestSender backupTestRequestSender;
private readonly ITestRuntimeProvider backupTestHostManager;

private ITestRuntimeProvider testHostManager;
private IRequestData requestData;

private readonly IFileHelper fileHelper;
private readonly IDataSerializer dataSerializer;
private bool isCommunicationEstablished;

private ManualResetEvent proxyOperationManagerInitializedEvent = new ManualResetEvent(false);
private ProxyOperationManager proxyOperationManager = null;
private ITestDiscoveryEventsHandler2 baseTestDiscoveryEventsHandler;
private bool skipDefaultAdapters;
private readonly IFileHelper fileHelper;

#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="ProxyDiscoveryManager"/> class.
/// </summary>
///
/// <param name="testSessionInfo">The test session info.</param>
/// <param name="runSettings">The run settings.</param>
/// <param name="backupRequestData">
/// The backup request data to be used to create a proxy operation manager should acquire
/// an existent proxy fail.
/// </param>
/// <param name="backupTestRequestSender">
/// The backup test request sender to be used to create a proxy operation manager should
/// acquire an existent proxy fail.
/// </param>
/// <param name="backupTestHostManager">
/// The backup testhost manager to be used to create a proxy operation manager should
/// acquire an existent proxy fail.
/// </param>
public ProxyDiscoveryManager(
TestSessionInfo testSessionInfo,
string runSettings,
IRequestData backupRequestData,
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
ITestRequestSender backupTestRequestSender,
ITestRuntimeProvider backupTestHostManager)
{
// Filling in test session info and proxy information.
this.testSessionInfo = testSessionInfo;
this.runSettings = runSettings;
this.backupRequestData = backupRequestData;
this.backupTestRequestSender = backupTestRequestSender;
this.backupTestHostManager = backupTestHostManager;

this.requestData = null;
this.testHostManager = null;
this.dataSerializer = JsonDataSerializer.Instance;
this.fileHelper = new FileHelper();
this.isCommunicationEstablished = false;
}

/// <summary>
/// Initializes a new instance of the <see cref="ProxyDiscoveryManager"/> class.
/// </summary>
Expand All @@ -55,9 +104,7 @@ public ProxyDiscoveryManager(
testHostManager,
JsonDataSerializer.Instance,
new FileHelper())
{
this.testHostManager = testHostManager;
}
{ }

/// <summary>
/// Initializes a new instance of the <see cref="ProxyDiscoveryManager"/> class.
Expand All @@ -81,14 +128,16 @@ internal ProxyDiscoveryManager(
IDataSerializer dataSerializer,
IFileHelper fileHelper)
{
this.dataSerializer = dataSerializer;
this.testHostManager = testHostManager;
this.isCommunicationEstablished = false;
this.requestData = requestData;
this.testHostManager = testHostManager;

this.dataSerializer = dataSerializer;
this.fileHelper = fileHelper;
this.isCommunicationEstablished = false;

// Create a new proxy operation manager.
this.proxyOperationManager = new ProxyOperationManager(requestData, requestSender, testHostManager, this);
this.proxyOperationManagerInitializedEvent.Set();
}

#endregion
Expand All @@ -104,6 +153,37 @@ public void Initialize(bool skipDefaultAdapters)
/// <inheritdoc/>
public void DiscoverTests(DiscoveryCriteria discoveryCriteria, ITestDiscoveryEventsHandler2 eventHandler)
{
if (this.proxyOperationManager == null)
{
// In case we have an active test session, we always prefer the already
// created proxies instead of the ones that need to be created on the spot.
this.proxyOperationManager = TestSessionPool.Instance.TakeProxy(
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
this.testSessionInfo,
discoveryCriteria.Sources.First(),
runSettings);

if (this.proxyOperationManager == null)
{
// If the proxy creation process based on test session info failed, then
// we'll proceed with the normal creation process as if no test session
// info was passed in in the first place.
//
// WARNING: This should not normally happen and it raises questions
// regarding the test session pool operation and consistency.
EqtTrace.Warning("ProxyDiscoveryManager creation with test session failed.");

this.proxyOperationManager = new ProxyOperationManager(
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
this.backupRequestData,
this.backupTestRequestSender,
this.backupTestHostManager,
this);
}

this.proxyOperationManagerInitializedEvent.Set();
this.testHostManager = this.proxyOperationManager.TestHostManager;
this.requestData = this.proxyOperationManager.RequestData;
}

this.baseTestDiscoveryEventsHandler = eventHandler;
try
{
Expand Down Expand Up @@ -149,6 +229,9 @@ public void DiscoverTests(DiscoveryCriteria discoveryCriteria, ITestDiscoveryEve
/// <inheritdoc/>
public void Abort()
{
// Make sure the proxy operation manager is initialized before anything.
this.proxyOperationManagerInitializedEvent.WaitOne();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can this happen? also, if the user doesn't request a discovery, this will deadlock. should this be a concern?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this (i.e. not issuing a discovery/run request) would cause a literal deadlock, rather indefinite waiting, which shouldn't be that bad given that calling Abort with nothing to actually abort is not really encouraged.

The need for introducing synchronization here is owed to the fact that the ProxyOperationManager is not necessarily initialized when the object is created. True, that's the case when we don't use test sessions, but with test sessions the ProxyOperationManager is only initialized when a discovery/run request is issued, making an abort before even running discovery to crash. Of course, we can do some null checking and have Abort do nothing if the proxy is not initialized yet, but in this way at least there's visible consequences to this behavior and maybe that can direct the caller to fix it rather than just ignore the abort request on TP side.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is somewhat concerning. I could imagine writing code:

using (var _ = ct.Register(() => tp.Abort()))
{
  ct.ThrowIfCancellationRequested();
  tp.DiscoverTests(...);
}

where if cancellation occurs I might not trigger a discover/tests call, but will have registered an abort handler.

in general that's the result of the existing sync API because Discover/Run blocks the caller's thread. as a result we register a cancellation token handler before calling Discover/Run methods. this means that it would be possible for TP to receive an Abort call before a Discover/Run calls

if TP would take a cancellation token or send an event that a Discovery/Run became abortable, we could ensure to never call Abort before Discover/Run

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's best to keep it simple and replace the ManualResetEvent with null checks then.


// Cancel fast, try to stop testhost deployment/launch
this.proxyOperationManager.CancellationTokenSource.Cancel();
this.Close();
Expand All @@ -157,7 +240,22 @@ public void Abort()
/// <inheritdoc/>
public void Close()
{
this.proxyOperationManager.Close();
// Make sure the proxy operation manager is initialized before anything.
this.proxyOperationManagerInitializedEvent.WaitOne();

// In compatibility mode (no test session used) we don't share the testhost
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved
// between test discovery and test run. The testhost is closed upon
// successfully completing the operation it was spawned for.
//
// In contrast, the new workflow (using test sessions) means we should keep
// the testhost alive until explicitly closed by the test session owner.
if (this.testSessionInfo == null)
{
this.proxyOperationManager.Close();
return;
}

TestSessionPool.Instance.ReturnProxy(this.testSessionInfo, this.proxyOperationManager.Id);
}

/// <inheritdoc/>
Expand Down
Loading