Skip to content

Commit

Permalink
Added support for debugging external test processes (#2325)
Browse files Browse the repository at this point in the history
Added support for debugging external test processes
  • Loading branch information
cvpoienaru authored May 18, 2020
1 parent 59354e1 commit 3f18c87
Show file tree
Hide file tree
Showing 76 changed files with 1,161 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public void HandleRawMessage(string rawMessage)
}
}

public class RunEventHandler : ITestRunEventsHandler
public class RunEventHandler : ITestRunEventsHandler2
{
private AutoResetEvent waitHandle;

Expand Down Expand Up @@ -293,5 +293,11 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta
// No op
return -1;
}

public bool AttachDebuggerToProcess(int pid)
{
// No op
return false;
}
}
}
65 changes: 53 additions & 12 deletions src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,23 @@ namespace Microsoft.VisualStudio.TestPlatform.Client.DesignMode
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
using CommunicationUtilitiesResources = Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources;
using CoreUtilitiesConstants = Microsoft.VisualStudio.TestPlatform.CoreUtilities.Constants;
using ObjectModelConstants = Microsoft.VisualStudio.TestPlatform.ObjectModel.Constants;

/// <summary>
/// The design mode client.
/// </summary>
public class DesignModeClient : IDesignModeClient
{
private readonly ICommunicationManager communicationManager;

private readonly IDataSerializer dataSerializer;

private object ackLockObject = new object();

private ProtocolConfig protocolConfig = Constants.DefaultProtocolConfig;

private IEnvironment platformEnvironment;

protected Action<Message> onAckMessageReceived;

private TestSessionMessageLogger testSessionMessageLogger;
private object lockObject = new object();

protected Action<Message> onCustomTestHostLaunchAckReceived;
protected Action<Message> onAttachDebuggerAckRecieved;

/// <summary>
/// Initializes a new instance of the <see cref="DesignModeClient"/> class.
Expand Down Expand Up @@ -221,7 +219,13 @@ private void ProcessRequests(ITestRequestManager testRequestManager)

case MessageType.CustomTestHostLaunchCallback:
{
this.onAckMessageReceived?.Invoke(message);
this.onCustomTestHostLaunchAckReceived?.Invoke(message);
break;
}

case MessageType.EditorAttachDebuggerCallback:
{
this.onAttachDebuggerAckRecieved?.Invoke(message);
break;
}

Expand Down Expand Up @@ -264,11 +268,11 @@ private void ProcessRequests(ITestRequestManager testRequestManager)
/// </returns>
public int LaunchCustomHost(TestProcessStartInfo testProcessStartInfo, CancellationToken cancellationToken)
{
lock (ackLockObject)
lock (this.lockObject)
{
var waitHandle = new AutoResetEvent(false);
Message ackMessage = null;
this.onAckMessageReceived = (ackRawMessage) =>
this.onCustomTestHostLaunchAckReceived = (ackRawMessage) =>
{
ackMessage = ackRawMessage;
waitHandle.Set();
Expand All @@ -285,7 +289,7 @@ public int LaunchCustomHost(TestProcessStartInfo testProcessStartInfo, Cancellat

cancellationToken.ThrowTestPlatformExceptionIfCancellationRequested();

this.onAckMessageReceived = null;
this.onCustomTestHostLaunchAckReceived = null;

var ackPayload = this.dataSerializer.DeserializePayload<CustomHostLaunchAckPayload>(ackMessage);

Expand All @@ -300,6 +304,44 @@ public int LaunchCustomHost(TestProcessStartInfo testProcessStartInfo, Cancellat
}
}

/// <inheritdoc/>
public bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken)
{
// If an attach request is issued but there is no support for attaching on the other
// side of the communication channel, we simply return and let the caller know the
// request failed.
if (this.protocolConfig.Version < ObjectModelConstants.MinimumProtocolVersionWithDebugSupport)
{
return false;
}

lock (this.lockObject)
{
var waitHandle = new AutoResetEvent(false);
Message ackMessage = null;
this.onAttachDebuggerAckRecieved = (ackRawMessage) =>
{
ackMessage = ackRawMessage;
waitHandle.Set();
};

this.communicationManager.SendMessage(MessageType.EditorAttachDebugger, pid);

WaitHandle.WaitAny(new WaitHandle[] { waitHandle, cancellationToken.WaitHandle });

cancellationToken.ThrowTestPlatformExceptionIfCancellationRequested();
this.onAttachDebuggerAckRecieved = null;

var ackPayload = this.dataSerializer.DeserializePayload<EditorAttachDebuggerAckPayload>(ackMessage);
if (!ackPayload.Attached)
{
EqtTrace.Warning(ackPayload.ErrorMessage);
}

return ackPayload.Attached;
}
}

/// <summary>
/// Send the raw messages to IDE
/// </summary>
Expand Down Expand Up @@ -439,7 +481,6 @@ public void Dispose()
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
}

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.VisualStudio.TestPlatform.Client.DesignMode
/// <summary>
/// DesignMode TestHost Launcher for hosting of test process
/// </summary>
internal class DesignModeTestHostLauncher : ITestHostLauncher
internal class DesignModeTestHostLauncher : ITestHostLauncher2
{
private readonly IDesignModeClient designModeClient;

Expand All @@ -26,6 +26,18 @@ public DesignModeTestHostLauncher(IDesignModeClient designModeClient)
/// <inheritdoc/>
public virtual bool IsDebug => false;

/// <inheritdoc/>
public bool AttachDebuggerToProcess(int pid)
{
return this.designModeClient.AttachDebuggerToProcess(pid, CancellationToken.None);
}

/// <inheritdoc/>
public bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken)
{
return this.designModeClient.AttachDebuggerToProcess(pid, cancellationToken);
}

/// <inheritdoc/>
public int LaunchTestHost(TestProcessStartInfo defaultTestHostStartInfo)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ namespace Microsoft.VisualStudio.TestPlatform.Client.DesignMode
public static class DesignModeTestHostLauncherFactory
{
private static ITestHostLauncher defaultLauncher;

private static ITestHostLauncher debugLauncher;

public static ITestHostLauncher GetCustomHostLauncherForTestRun(IDesignModeClient designModeClient, TestRunRequestPayload testRunRequestPayload)
{
ITestHostLauncher testHostLauncher = null;

if (!testRunRequestPayload.DebuggingEnabled)
{
testHostLauncher = defaultLauncher = defaultLauncher ?? new DesignModeTestHostLauncher(designModeClient);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public interface IDesignModeClient : IDisposable
/// <returns>Process id of the launched test host.</returns>
int LaunchCustomHost(TestProcessStartInfo defaultTestHostStartInfo, CancellationToken cancellationToken);

/// <summary>
/// Attach debugger to an already running process.
/// </summary>
/// <param name="pid">Process ID of the process to which the debugger should be attached.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="true"/> if the debugger was successfully attached to the requested process, <see cref="false"/> otherwise.</returns>
bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken);

/// <summary>
/// Handles parent process exit
/// </summary>
Expand Down
13 changes: 11 additions & 2 deletions src/Microsoft.TestPlatform.Client/Execution/TestRunRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@ namespace Microsoft.VisualStudio.TestPlatform.Client.Execution
using Microsoft.VisualStudio.TestPlatform.Common.Telemetry;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
using Microsoft.VisualStudio.TestPlatform.Utilities;

using ClientResources = Microsoft.VisualStudio.TestPlatform.Client.Resources.Resources;
using CommunicationObjectModel = Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel;

public class TestRunRequest : ITestRunRequest, ITestRunEventsHandler
public class TestRunRequest : ITestRunRequest, ITestRunEventsHandler2
{
/// <summary>
/// The criteria/config for this test run request.
Expand Down Expand Up @@ -659,6 +660,14 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta
return processId;
}

/// <inheritdoc />
public bool AttachDebuggerToProcess(int pid)
{
return this.testRunCriteria.TestHostLauncher is ITestHostLauncher2 launcher
? launcher.AttachDebuggerToProcess(pid)
: false;
}

/// <summary>
/// Dispose the run
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

using System.Linq;
using Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework.Utilities;
using Microsoft.VisualStudio.TestPlatform.Common.Interfaces;
using Microsoft.VisualStudio.TestPlatform.Common.Logging;
Expand Down Expand Up @@ -49,6 +49,60 @@ protected TestExecutorExtensionManager(

#endregion

#region Private Methods
/// <summary>
/// Merges two test extension lists.
/// </summary>
///
/// <typeparam name="TExecutor1">Type of first test extension.</typeparam>
/// <typeparam name="TExecutor2">Type of second test extension.</typeparam>
/// <typeparam name="TValue">Type of the value used in the lazy extension expression.</typeparam>
///
/// <param name="testExtensions1">First test extension list.</param>
/// <param name="testExtensions2">Second test extension list.</param>
///
/// <returns>A merged list of test extensions.</returns>
private static IEnumerable<LazyExtension<TExecutor1, TValue>> MergeTestExtensionLists<TExecutor1, TExecutor2, TValue>(
IEnumerable<LazyExtension<TExecutor1, TValue>> testExtensions1,
IEnumerable<LazyExtension<TExecutor2, TValue>> testExtensions2) where TExecutor1 : ITestExecutor where TExecutor2 : TExecutor1
{
if (!testExtensions2.Any())
{
return testExtensions1;
}

var mergedTestExtensions = new List<LazyExtension<TExecutor1, TValue>>();
var cache = new Dictionary<string, LazyExtension<TExecutor1, TValue>>();

// Create the cache used for merging by adding all extensions from the first list.
foreach (var testExtension in testExtensions1)
{
cache.Add(testExtension.TestPluginInfo.IdentifierData, testExtension);
}

// Update the cache with extensions from the second list. Should there be any conflict
// we prefer the second extension to the first.
foreach (var testExtension in testExtensions2)
{
if (cache.ContainsKey(testExtension.TestPluginInfo.IdentifierData))
{
cache[testExtension.TestPluginInfo.IdentifierData] =
new LazyExtension<TExecutor1, TValue>(
(TExecutor1)testExtension.Value, testExtension.Metadata);
}
}

// Create the merged test extensions list from the cache.
foreach (var kvp in cache)
{
mergedTestExtensions.Add(kvp.Value);
}

return mergedTestExtensions;
}

#endregion

#region Factory Methods

/// <summary>
Expand All @@ -63,17 +117,37 @@ internal static TestExecutorExtensionManager Create()
{
if (testExecutorExtensionManager == null)
{
IEnumerable<LazyExtension<ITestExecutor, Dictionary<string, object>>> unfilteredTestExtensions;
IEnumerable<LazyExtension<ITestExecutor, ITestExecutorCapabilities>> testExtensions;
IEnumerable<LazyExtension<ITestExecutor, Dictionary<string, object>>> unfilteredTestExtensions1;
IEnumerable<LazyExtension<ITestExecutor2, Dictionary<string, object>>> unfilteredTestExtensions2;
IEnumerable<LazyExtension<ITestExecutor, ITestExecutorCapabilities>> testExtensions1;
IEnumerable<LazyExtension<ITestExecutor2, ITestExecutorCapabilities>> testExtensions2;

// Get all extensions for ITestExecutor.
TestPluginManager.Instance
.GetSpecificTestExtensions<TestExecutorPluginInformation, ITestExecutor, ITestExecutorCapabilities, TestExecutorMetadata>(
TestPlatformConstants.TestAdapterEndsWithPattern,
out unfilteredTestExtensions,
out testExtensions);
out unfilteredTestExtensions1,
out testExtensions1);

// Get all extensions for ITestExecutor2.
TestPluginManager.Instance
.GetSpecificTestExtensions<TestExecutorPluginInformation2, ITestExecutor2, ITestExecutorCapabilities, TestExecutorMetadata>(
TestPlatformConstants.TestAdapterEndsWithPattern,
out unfilteredTestExtensions2,
out testExtensions2);

// Merge the extension lists.
var mergedUnfilteredTestExtensions = TestExecutorExtensionManager.MergeTestExtensionLists(
unfilteredTestExtensions1,
unfilteredTestExtensions2);

var mergedTestExtensions = TestExecutorExtensionManager.MergeTestExtensionLists(
testExtensions1,
testExtensions2);

// Create the TestExecutorExtensionManager using the merged extension list.
testExecutorExtensionManager = new TestExecutorExtensionManager(
unfilteredTestExtensions, testExtensions, TestSessionMessageLogger.Instance);
mergedUnfilteredTestExtensions, mergedTestExtensions, TestSessionMessageLogger.Instance);
}
}
}
Expand All @@ -92,20 +166,39 @@ internal static TestExecutorExtensionManager Create()
/// </remarks>
internal static TestExecutorExtensionManager GetExecutionExtensionManager(string extensionAssembly)
{
IEnumerable<LazyExtension<ITestExecutor, Dictionary<string, object>>> unfilteredTestExtensions;
IEnumerable<LazyExtension<ITestExecutor, ITestExecutorCapabilities>> testExtensions;
IEnumerable<LazyExtension<ITestExecutor, Dictionary<string, object>>> unfilteredTestExtensions1;
IEnumerable<LazyExtension<ITestExecutor2, Dictionary<string, object>>> unfilteredTestExtensions2;
IEnumerable<LazyExtension<ITestExecutor, ITestExecutorCapabilities>> testExtensions1;
IEnumerable<LazyExtension<ITestExecutor2, ITestExecutorCapabilities>> testExtensions2;

// Get all extensions for ITestExecutor.
TestPluginManager.Instance
.GetTestExtensions<TestExecutorPluginInformation, ITestExecutor, ITestExecutorCapabilities, TestExecutorMetadata>(
extensionAssembly,
out unfilteredTestExtensions,
out testExtensions);
out unfilteredTestExtensions1,
out testExtensions1);

// Get all extensions for ITestExecutor2.
TestPluginManager.Instance
.GetTestExtensions<TestExecutorPluginInformation2, ITestExecutor2, ITestExecutorCapabilities, TestExecutorMetadata>(
extensionAssembly,
out unfilteredTestExtensions2,
out testExtensions2);

// Merge the extension lists.
var mergedUnfilteredTestExtensions = TestExecutorExtensionManager.MergeTestExtensionLists(
unfilteredTestExtensions1,
unfilteredTestExtensions2);

var mergedTestExtensions = TestExecutorExtensionManager.MergeTestExtensionLists(
testExtensions1,
testExtensions2);

// TODO: This can be optimized - The base class's populate map would be called repeatedly for the same extension assembly.
// Have a single instance of TestExecutorExtensionManager that keeps populating the map iteratively.
return new TestExecutorExtensionManager(
unfilteredTestExtensions,
testExtensions,
mergedUnfilteredTestExtensions,
mergedTestExtensions,
TestSessionMessageLogger.Instance);
}

Expand Down
Loading

0 comments on commit 3f18c87

Please sign in to comment.