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

Added support for debugging external test processes #2325

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
74f547c
[WIP] Initial support for debugging external test processes
cvpoienaru Feb 11, 2020
341b0ea
[WIP] Initial support for debugging external test processes
cvpoienaru Feb 11, 2020
b03b3f4
Merge branch 'copoiena/debugging-external-test-processes-2' of https:…
cvpoienaru Feb 17, 2020
11a0af2
Merge branch 'copoiena/debugging-external-test-processes-2' of https:…
cvpoienaru Feb 17, 2020
9627709
Merge branch 'copoiena/debugging-external-test-processes-2' of https:…
cvpoienaru Feb 18, 2020
02ea724
Merge branch 'copoiena/debugging-external-test-processes-2' of https:…
cvpoienaru Feb 18, 2020
5474a64
Merge branch 'copoiena/debugging-external-test-processes-2' of https:…
cvpoienaru Feb 18, 2020
72a6abc
Added support for the ITestExecutor2 interface
cvpoienaru Feb 19, 2020
d572add
Added support for ITestRunEventsHandler2
cvpoienaru Feb 24, 2020
3a12437
Final changes on test platform
cvpoienaru Feb 25, 2020
87ed797
Changed failing test to conform to new workflow
cvpoienaru Feb 26, 2020
57ba62f
Changed is-as to as when casting
cvpoienaru Feb 27, 2020
3e40728
Implemented IVsTestConsoleWrapper2 interface & changed the way the te…
cvpoienaru Mar 2, 2020
c3033e3
Changed as conversion to is-as
cvpoienaru Mar 2, 2020
52a9a7b
Disabled unnecessary parenthesis warning
cvpoienaru Mar 2, 2020
798c7bf
Unsubscribed from the correct event
cvpoienaru Mar 4, 2020
3ea3e3d
Changed AttachDebuggerToProcess messages between IDE and testhost
cvpoienaru Mar 4, 2020
edabd38
Ignored custom host launcher tests
cvpoienaru Mar 5, 2020
99b4591
Reimplemented VsTestConsoleWrapper calls for ITestHostLauncher2
cvpoienaru Mar 6, 2020
bbbd645
Removed IVsTestConsoleWrapper2 interface
cvpoienaru Mar 6, 2020
0af1e41
Fixed acceptance/performance tests
cvpoienaru Mar 6, 2020
ad2cb01
Fixed profiling workflow
cvpoienaru Mar 9, 2020
68e4c30
Fixed dotnet testhost launcher
cvpoienaru Mar 9, 2020
a0cad9b
Various code review fixes
cvpoienaru Mar 10, 2020
78b93aa
Fixed minor style issues
cvpoienaru Mar 10, 2020
4ec216c
Introduced ITestRunEventsHandler2 interface
cvpoienaru Mar 10, 2020
b81b1cd
Various code review changes
cvpoienaru Mar 11, 2020
50bd7d5
Fixed code review comments
cvpoienaru Mar 16, 2020
95b553a
Fixed compilation error
cvpoienaru Mar 16, 2020
bfe2386
Fixed test compilation issues
cvpoienaru Mar 16, 2020
9440e3f
Changed non-customer facing interfaces to internal from public
cvpoienaru Mar 16, 2020
5142161
Switched interface visibility back to public
cvpoienaru Mar 18, 2020
729b7f1
Resolved some code review comments
cvpoienaru Mar 19, 2020
b33c50f
Added localized resource string for failing to attach the debugger to…
cvpoienaru Mar 19, 2020
330083f
Merge branch 'master' into copoiena/debugging-external-test-processes-2
cvpoienaru Mar 20, 2020
c6f9c93
Fixed backward compatibility issues with older testhosts
cvpoienaru Apr 5, 2020
0d87d35
Fixed broken unit tests
cvpoienaru Apr 5, 2020
fa2bff1
Fixed VsTestConsoleRequestSender protocol version
cvpoienaru Apr 8, 2020
c409757
Fixed protocol version in unit tests
cvpoienaru Apr 8, 2020
fc9d044
Fixed compatibility issues between an older VS and the latest Test.SD…
cvpoienaru Apr 10, 2020
777657a
Removed Ignore attribute from unit test
cvpoienaru Apr 23, 2020
97a804c
Merge branch 'master' into copoiena/debugging-external-test-processes-2
cvpoienaru Apr 23, 2020
232918d
Removed last Ignore attribute from old unit tests
cvpoienaru Apr 23, 2020
aa2b38e
Implemented a selection algorithm for ITestExecutor and ITestExecutor…
cvpoienaru Apr 29, 2020
e0f4891
Fixed unit test failures
cvpoienaru May 4, 2020
0e20565
Merge branch 'master' into copoiena/debugging-external-test-processes-2
cvpoienaru May 4, 2020
581d97d
Changed test extension conflict from error to warning
cvpoienaru May 6, 2020
5dd0b38
Fixed some code review comments
cvpoienaru May 14, 2020
4dfbce3
Fixed compat issues with old versions of VS
cvpoienaru May 14, 2020
9f15160
Changed test execution protocol message names
cvpoienaru May 15, 2020
234a53c
Merge branch 'master' into copoiena/debugging-external-test-processes-2
cvpoienaru May 15, 2020
cf2261b
Final code review comments
cvpoienaru May 18, 2020
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 @@ -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);
cvpoienaru marked this conversation as resolved.
Show resolved Hide resolved

/// <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