diff --git a/samples/Microsoft.TestPlatform.TranslationLayer.E2ETest/Program.cs b/samples/Microsoft.TestPlatform.TranslationLayer.E2ETest/Program.cs index faa74b3763..5d6a996dc9 100644 --- a/samples/Microsoft.TestPlatform.TranslationLayer.E2ETest/Program.cs +++ b/samples/Microsoft.TestPlatform.TranslationLayer.E2ETest/Program.cs @@ -243,7 +243,7 @@ public void HandleRawMessage(string rawMessage) } } - public class RunEventHandler : ITestRunEventsHandler + public class RunEventHandler : ITestRunEventsHandler2 { private AutoResetEvent waitHandle; @@ -293,5 +293,11 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta // No op return -1; } + + public bool AttachDebuggerToProcess(int pid) + { + // No op + return false; + } } } diff --git a/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs b/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs index df45f69634..e4366b4c8b 100644 --- a/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs +++ b/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeClient.cs @@ -24,6 +24,7 @@ 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; /// /// The design mode client. @@ -31,18 +32,15 @@ namespace Microsoft.VisualStudio.TestPlatform.Client.DesignMode 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 onAckMessageReceived; - private TestSessionMessageLogger testSessionMessageLogger; + private object lockObject = new object(); + + protected Action onCustomTestHostLaunchAckReceived; + protected Action onAttachDebuggerAckRecieved; /// /// Initializes a new instance of the class. @@ -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; } @@ -264,11 +268,11 @@ private void ProcessRequests(ITestRequestManager testRequestManager) /// 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(); @@ -285,7 +289,7 @@ public int LaunchCustomHost(TestProcessStartInfo testProcessStartInfo, Cancellat cancellationToken.ThrowTestPlatformExceptionIfCancellationRequested(); - this.onAckMessageReceived = null; + this.onCustomTestHostLaunchAckReceived = null; var ackPayload = this.dataSerializer.DeserializePayload(ackMessage); @@ -300,6 +304,44 @@ public int LaunchCustomHost(TestProcessStartInfo testProcessStartInfo, Cancellat } } + /// + 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(ackMessage); + if (!ackPayload.Attached) + { + EqtTrace.Warning(ackPayload.ErrorMessage); + } + + return ackPayload.Attached; + } + } + /// /// Send the raw messages to IDE /// @@ -439,7 +481,6 @@ public void Dispose() // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); } - #endregion } } diff --git a/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeTestHostLauncher.cs b/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeTestHostLauncher.cs index bc84515c49..33c37b6995 100644 --- a/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeTestHostLauncher.cs +++ b/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeTestHostLauncher.cs @@ -10,7 +10,7 @@ namespace Microsoft.VisualStudio.TestPlatform.Client.DesignMode /// /// DesignMode TestHost Launcher for hosting of test process /// - internal class DesignModeTestHostLauncher : ITestHostLauncher + internal class DesignModeTestHostLauncher : ITestHostLauncher2 { private readonly IDesignModeClient designModeClient; @@ -26,6 +26,18 @@ public DesignModeTestHostLauncher(IDesignModeClient designModeClient) /// public virtual bool IsDebug => false; + /// + public bool AttachDebuggerToProcess(int pid) + { + return this.designModeClient.AttachDebuggerToProcess(pid, CancellationToken.None); + } + + /// + public bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken) + { + return this.designModeClient.AttachDebuggerToProcess(pid, cancellationToken); + } + /// public int LaunchTestHost(TestProcessStartInfo defaultTestHostStartInfo) { diff --git a/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeTestHostLauncherFactory.cs b/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeTestHostLauncherFactory.cs index 6cceb008fb..8edec80450 100644 --- a/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeTestHostLauncherFactory.cs +++ b/src/Microsoft.TestPlatform.Client/DesignMode/DesignModeTestHostLauncherFactory.cs @@ -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); diff --git a/src/Microsoft.TestPlatform.Client/DesignMode/IDesignModeClient.cs b/src/Microsoft.TestPlatform.Client/DesignMode/IDesignModeClient.cs index 62845a06c6..1b633c0a67 100644 --- a/src/Microsoft.TestPlatform.Client/DesignMode/IDesignModeClient.cs +++ b/src/Microsoft.TestPlatform.Client/DesignMode/IDesignModeClient.cs @@ -28,6 +28,14 @@ public interface IDesignModeClient : IDisposable /// Process id of the launched test host. int LaunchCustomHost(TestProcessStartInfo defaultTestHostStartInfo, CancellationToken cancellationToken); + /// + /// Attach debugger to an already running process. + /// + /// Process ID of the process to which the debugger should be attached. + /// The cancellation token. + /// if the debugger was successfully attached to the requested process, otherwise. + bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken); + /// /// Handles parent process exit /// diff --git a/src/Microsoft.TestPlatform.Client/Execution/TestRunRequest.cs b/src/Microsoft.TestPlatform.Client/Execution/TestRunRequest.cs index 0c4a3c9fbc..fdce95cde3 100644 --- a/src/Microsoft.TestPlatform.Client/Execution/TestRunRequest.cs +++ b/src/Microsoft.TestPlatform.Client/Execution/TestRunRequest.cs @@ -13,8 +13,10 @@ 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; @@ -22,9 +24,8 @@ namespace Microsoft.VisualStudio.TestPlatform.Client.Execution 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 { /// /// The criteria/config for this test run request. @@ -659,6 +660,14 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta return processId; } + /// + public bool AttachDebuggerToProcess(int pid) + { + return this.testRunCriteria.TestHostLauncher is ITestHostLauncher2 launcher + ? launcher.AttachDebuggerToProcess(pid) + : false; + } + /// /// Dispose the run /// diff --git a/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestExecutorExtensionManager.cs b/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestExecutorExtensionManager.cs index 30c822c6a0..ac7f7e3180 100644 --- a/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestExecutorExtensionManager.cs +++ b/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestExecutorExtensionManager.cs @@ -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; @@ -49,6 +49,60 @@ protected TestExecutorExtensionManager( #endregion + #region Private Methods + /// + /// Merges two test extension lists. + /// + /// + /// Type of first test extension. + /// Type of second test extension. + /// Type of the value used in the lazy extension expression. + /// + /// First test extension list. + /// Second test extension list. + /// + /// A merged list of test extensions. + private static IEnumerable> MergeTestExtensionLists( + IEnumerable> testExtensions1, + IEnumerable> testExtensions2) where TExecutor1 : ITestExecutor where TExecutor2 : TExecutor1 + { + if (!testExtensions2.Any()) + { + return testExtensions1; + } + + var mergedTestExtensions = new List>(); + var cache = new Dictionary>(); + + // 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)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 /// @@ -63,17 +117,37 @@ internal static TestExecutorExtensionManager Create() { if (testExecutorExtensionManager == null) { - IEnumerable>> unfilteredTestExtensions; - IEnumerable> testExtensions; + IEnumerable>> unfilteredTestExtensions1; + IEnumerable>> unfilteredTestExtensions2; + IEnumerable> testExtensions1; + IEnumerable> testExtensions2; + // Get all extensions for ITestExecutor. TestPluginManager.Instance .GetSpecificTestExtensions( TestPlatformConstants.TestAdapterEndsWithPattern, - out unfilteredTestExtensions, - out testExtensions); + out unfilteredTestExtensions1, + out testExtensions1); + + // Get all extensions for ITestExecutor2. + TestPluginManager.Instance + .GetSpecificTestExtensions( + 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); } } } @@ -92,20 +166,39 @@ internal static TestExecutorExtensionManager Create() /// internal static TestExecutorExtensionManager GetExecutionExtensionManager(string extensionAssembly) { - IEnumerable>> unfilteredTestExtensions; - IEnumerable> testExtensions; + IEnumerable>> unfilteredTestExtensions1; + IEnumerable>> unfilteredTestExtensions2; + IEnumerable> testExtensions1; + IEnumerable> testExtensions2; + // Get all extensions for ITestExecutor. TestPluginManager.Instance .GetTestExtensions( extensionAssembly, - out unfilteredTestExtensions, - out testExtensions); + out unfilteredTestExtensions1, + out testExtensions1); + + // Get all extensions for ITestExecutor2. + TestPluginManager.Instance + .GetTestExtensions( + 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); } diff --git a/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestPluginCache.cs b/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestPluginCache.cs index b804aef0c9..b2905830f5 100644 --- a/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestPluginCache.cs +++ b/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestPluginCache.cs @@ -342,8 +342,9 @@ internal IEnumerable DefaultExtensionPaths internal Dictionary GetTestExtensions(string extensionAssembly) where TPluginInfo : TestPluginInformation { // Check if extensions from this assembly have already been discovered. - var extensions = this.TestExtensions?.GetExtensionsDiscoveredFromAssembly(this.TestExtensions.GetTestExtensionCache(), extensionAssembly); - + var extensions = this.TestExtensions?.GetExtensionsDiscoveredFromAssembly( + this.TestExtensions.GetTestExtensionCache(), + extensionAssembly); if (extensions != null) { @@ -569,6 +570,9 @@ private void LogExtensions() var executors = this.TestExtensions.TestExecutors != null ? string.Join(",", this.TestExtensions.TestExecutors.Keys.ToArray()) : null; EqtTrace.Verbose("TestPluginCache: Executors are '{0}'.", executors); + var executors2 = this.TestExtensions.TestExecutors2 != null ? string.Join(",", this.TestExtensions.TestExecutors2.Keys.ToArray()) : null; + EqtTrace.Verbose("TestPluginCache: Executors2 are '{0}'.", executors2); + var settingsProviders = this.TestExtensions.TestSettingsProviders != null ? string.Join(",", this.TestExtensions.TestSettingsProviders.Keys.ToArray()) : null; EqtTrace.Verbose("TestPluginCache: Setting providers are '{0}'.", settingsProviders); diff --git a/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestPluginDiscoverer.cs b/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestPluginDiscoverer.cs index aa80c44cc3..5b18dedfb4 100644 --- a/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestPluginDiscoverer.cs +++ b/src/Microsoft.TestPlatform.Common/ExtensionFramework/TestPluginDiscoverer.cs @@ -247,7 +247,7 @@ private void GetTestExtensionFromType( if (extensionCollection.ContainsKey(pluginInfo.IdentifierData)) { - EqtTrace.Error( + EqtTrace.Warning( "TryGetTestExtensionFromType: Discovered multiple test extensions with identifier data '{0}'; keeping the first one.", pluginInfo.IdentifierData); } diff --git a/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/LazyExtension.cs b/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/LazyExtension.cs index d89b7364ce..61a78b164f 100644 --- a/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/LazyExtension.cs +++ b/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/LazyExtension.cs @@ -130,6 +130,8 @@ internal bool IsExtensionCreated } } + internal TestPluginInformation TestPluginInfo => this.testPluginInfo; + /// /// Gets the test extension instance. /// diff --git a/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/TestExecutorPluginInformation.cs b/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/TestExecutorPluginInformation.cs index 17437af0ba..b768494fb2 100644 --- a/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/TestExecutorPluginInformation.cs +++ b/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/TestExecutorPluginInformation.cs @@ -19,4 +19,19 @@ public TestExecutorPluginInformation(Type testExecutorType) { } } + + /// + /// The test executor 2 plugin information. + /// + internal class TestExecutorPluginInformation2 : TestExtensionPluginInformation + { + /// + /// Default constructor + /// + /// The test Executor Type. + public TestExecutorPluginInformation2(Type testExecutorType) + : base(testExecutorType) + { + } + } } diff --git a/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/TestExtensions.cs b/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/TestExtensions.cs index dc69612880..7ad4ddede3 100644 --- a/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/TestExtensions.cs +++ b/src/Microsoft.TestPlatform.Common/ExtensionFramework/Utilities/TestExtensions.cs @@ -39,6 +39,16 @@ public class TestExtensions /// internal bool AreTestExecutorsCached { get; set; } + /// + /// Gets or sets test executor 2 extensions. + /// + internal Dictionary TestExecutors2 { get; set; } + + /// + /// Gets or sets a value indicating whether are test executors 2 cached. + /// + internal bool AreTestExecutors2Cached { get; set; } + /// /// Gets or sets test setting provider extensions. /// @@ -144,6 +154,10 @@ internal TestExtensions GetExtensionsDiscoveredFromAssembly(string extensionAsse this.GetExtensionsDiscoveredFromAssembly( this.TestExecutors, extensionAssembly); + testExtensions.TestExecutors2 = + this.GetExtensionsDiscoveredFromAssembly( + this.TestExecutors2, + extensionAssembly); testExtensions.TestSettingsProviders = this.GetExtensionsDiscoveredFromAssembly( this.TestSettingsProviders, @@ -161,8 +175,13 @@ internal TestExtensions GetExtensionsDiscoveredFromAssembly(string extensionAsse this.DataCollectors, extensionAssembly); - if (testExtensions.TestDiscoverers.Any() || testExtensions.TestExecutors.Any() || testExtensions.TestSettingsProviders.Any() || - testExtensions.TestLoggers.Any() || testExtensions.TestHosts.Any() || testExtensions.DataCollectors.Any()) + if (testExtensions.TestDiscoverers.Any() + || testExtensions.TestExecutors.Any() + || testExtensions.TestExecutors2.Any() + || testExtensions.TestSettingsProviders.Any() + || testExtensions.TestLoggers.Any() + || testExtensions.TestHosts.Any() + || testExtensions.DataCollectors.Any()) { // This extension has already been discovered. return testExtensions; @@ -181,6 +200,10 @@ internal Dictionary GetTestExtensionCache() wh { return (Dictionary)(object)this.TestExecutors; } + else if (typeof(TPluginInfo) == typeof(TestExecutorPluginInformation2)) + { + return (Dictionary)(object)this.TestExecutors2; + } else if (typeof(TPluginInfo) == typeof(TestLoggerPluginInformation)) { return (Dictionary)(object)this.TestLoggers; @@ -219,6 +242,10 @@ internal bool AreTestExtensionsCached() where TPluginInfo : TestPlu { return this.AreTestExecutorsCached; } + else if (typeof(TPluginInfo) == typeof(TestExecutorPluginInformation2)) + { + return this.AreTestExecutors2Cached; + } else if (typeof(TPluginInfo) == typeof(TestLoggerPluginInformation)) { return this.AreTestLoggersCached; @@ -254,6 +281,10 @@ internal void SetTestExtensionsCacheStatus() where TPluginInfo : Te { this.AreTestExecutorsCached = true; } + else if (typeof(TPluginInfo) == typeof(TestExecutorPluginInformation2)) + { + this.AreTestExecutors2Cached = true; + } else if (typeof(TPluginInfo) == typeof(TestLoggerPluginInformation)) { this.AreTestLoggersCached = true; @@ -279,6 +310,7 @@ internal void InvalidateCache() { this.AreTestDiscoverersCached = false; this.AreTestExecutorsCached = false; + this.AreTestExecutors2Cached = false; this.AreTestLoggersCached = false; this.AreTestSettingsProvidersCached = false; this.AreTestHostsCached = false; @@ -299,7 +331,9 @@ internal void InvalidateCache() /// /// The . of extensions discovered in assembly /// - internal Dictionary GetExtensionsDiscoveredFromAssembly(Dictionary extensionCollection, string extensionAssembly) + internal Dictionary GetExtensionsDiscoveredFromAssembly( + Dictionary extensionCollection, + string extensionAssembly) { var extensions = new Dictionary(); if (extensionCollection != null) @@ -332,6 +366,10 @@ private void SetTestExtensionCache(Dictionary { this.TestExecutors = (Dictionary)(object)testPluginInfos; } + else if (typeof(TPluginInfo) == typeof(TestExecutorPluginInformation2)) + { + this.TestExecutors2 = (Dictionary)(object)testPluginInfos; + } else if (typeof(TPluginInfo) == typeof(TestLoggerPluginInformation)) { this.TestLoggers = (Dictionary)(object)testPluginInfos; diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/EventHandlers/TestRunEventsHandler.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/EventHandlers/TestRunEventsHandler.cs index 8a8e2acd32..3dc09a5140 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/EventHandlers/TestRunEventsHandler.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/EventHandlers/TestRunEventsHandler.cs @@ -13,7 +13,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.EventHandle /// /// The test run events handler. /// - public class TestRunEventsHandler : ITestRunEventsHandler + public class TestRunEventsHandler : ITestRunEventsHandler2 { private ITestRequestHandler requestHandler; @@ -95,5 +95,12 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta EqtTrace.Info("Sending LaunchProcessWithDebuggerAttached on additional test process: {0}", testProcessStartInfo?.FileName); return this.requestHandler.LaunchProcessWithDebuggerAttached(testProcessStartInfo); } + + /// + public bool AttachDebuggerToProcess(int pid) + { + EqtTrace.Info("Sending AttachDebuggerToProcess on additional test process with pid: {0}", pid); + return this.requestHandler.AttachDebuggerToProcess(pid); + } } } diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestRequestHandler.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestRequestHandler.cs index 095ab878e2..8908a72fc7 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestRequestHandler.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Interfaces/ITestRequestHandler.cs @@ -86,5 +86,12 @@ public interface ITestRequestHandler : IDisposable /// Process start info /// ProcessId of the launched process int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessStartInfo); + + /// + /// Attach debugger to an already running process. + /// + /// Process ID of the process to which the debugger should be attached. + /// if the debugger was successfully attached to the requested process, otherwise. + bool AttachDebuggerToProcess(int pid); } } diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/MessageType.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/MessageType.cs index eb489480be..52a74e7f0b 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/MessageType.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Messages/MessageType.cs @@ -148,6 +148,26 @@ public static class MessageType /// public const string LaunchAdapterProcessWithDebuggerAttachedCallback = "TestExecution.LaunchAdapterProcessWithDebuggerAttachedCallback"; + /// + /// Attach debugger to process. + /// + public const string AttachDebugger = "TestExecution.AttachDebugger"; + + /// + /// Attach debugger to process callback. + /// + public const string AttachDebuggerCallback = "TestExecution.AttachDebuggerCallback"; + + /// + /// Attach debugger to process. + /// + public const string EditorAttachDebugger = "TestExecution.EditorAttachDebugger"; + + /// + /// Attach debugger to process callback. + /// + public const string EditorAttachDebuggerCallback = "TestExecution.EditorAttachDebuggerCallback"; + /// /// Data Collection Message /// diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/Resources.Designer.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/Resources.Designer.cs index d9740f7d99..9ccddada2f 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/Resources.Designer.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/Resources.Designer.cs @@ -10,7 +10,6 @@ namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources { using System; - using System.Reflection; /// @@ -20,7 +19,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { @@ -40,7 +39,7 @@ internal Resources() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources", typeof(Resources).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -79,6 +78,15 @@ public static string AbortedTestRun { } } + /// + /// Looks up a localized string similar to Cannot attach the debugger to the default test host.. + /// + public static string AttachDebuggerToDefaultTestHostFailure { + get { + return ResourceManager.GetString("AttachDebuggerToDefaultTestHostFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to An existing connection was forcibly closed by the remote host.. /// diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/Resources.resx b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/Resources.resx index 84a2ab4195..b98ce64332 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/Resources.resx +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/Resources.resx @@ -144,4 +144,7 @@ Test host process crashed + + Cannot attach the debugger to the default test host. + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.cs.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.cs.xlf index 8745af35a6..ac54905e0f 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.cs.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.cs.xlf @@ -81,6 +81,11 @@ Proces testovacího hostitele se chybově ukončil. + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.de.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.de.xlf index 7663d516f3..078b8e43f7 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.de.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.de.xlf @@ -81,6 +81,11 @@ Der Testhostprozess ist abgestürzt. + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.es.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.es.xlf index ccdbc6c093..2ea0f141bc 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.es.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.es.xlf @@ -81,6 +81,11 @@ Proceso de host de pruebas bloqueado + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.fr.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.fr.xlf index 5d287de96e..f2eec1579b 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.fr.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.fr.xlf @@ -81,6 +81,11 @@ Plantage du processus hôte de test + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.it.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.it.xlf index 91a86d85f4..fb0e96fa76 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.it.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.it.xlf @@ -81,6 +81,11 @@ Arresto anomalo del processo host di test + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ja.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ja.xlf index d057874909..c081c3a02d 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ja.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ja.xlf @@ -81,6 +81,11 @@ テストのホスト プロセスがクラッシュしました + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ko.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ko.xlf index 90e26aa615..3ccce228eb 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ko.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ko.xlf @@ -81,6 +81,11 @@ 테스트 호스트 프로세스 작동이 중단됨 + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.pl.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.pl.xlf index 35988d0e03..2b468231e6 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.pl.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.pl.xlf @@ -81,6 +81,11 @@ Wystąpiła awaria procesu hosta testu + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.pt-BR.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.pt-BR.xlf index 023c04e9b9..4b2ce75013 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.pt-BR.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.pt-BR.xlf @@ -81,6 +81,11 @@ Falha no processo do host de teste + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ru.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ru.xlf index b527ea6559..d53aaa5ac5 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ru.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.ru.xlf @@ -81,6 +81,11 @@ Сбой хост-процесса теста + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.tr.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.tr.xlf index a35b0eed0a..505c64f957 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.tr.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.tr.xlf @@ -81,6 +81,11 @@ Test ana işlemi kilitlendi + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.xlf index dce4249f3f..cfa1b10ad0 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.xlf @@ -43,6 +43,11 @@ Test host process crashed + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.zh-Hans.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.zh-Hans.xlf index 9d06983c19..83acac20e9 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.zh-Hans.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.zh-Hans.xlf @@ -81,6 +81,11 @@ 测试主机进程崩溃 + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.zh-Hant.xlf b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.zh-Hant.xlf index b84bc544a1..3ff28dbd78 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.zh-Hant.xlf +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/Resources/xlf/Resources.zh-Hant.xlf @@ -81,6 +81,11 @@ 測試主機處理序當機 + + Cannot attach the debugger to the default test host. + Cannot attach the debugger to the default test host. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs b/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs index b8f1b5ac1d..2edc603016 100644 --- a/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs +++ b/src/Microsoft.TestPlatform.CommunicationUtilities/TestRequestSender.cs @@ -12,8 +12,10 @@ namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; + using Microsoft.VisualStudio.TestPlatform.ObjectModel.Host; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using CommonResources = Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources.Resources; + using ObjectModelConstants = Microsoft.VisualStudio.TestPlatform.ObjectModel.Constants; /// /// Test request sender implementation. @@ -47,24 +49,35 @@ public class TestRequestSender : ITestRequestSender private string clientExitErrorMessage; // Set default to 1, if protocol version check does not happen - // that implies host is using version 1 + // that implies host is using version 1. private int protocolVersion = 1; - private int highestSupportedVersion = 2; + private int highestSupportedVersion = 3; + private TestHostConnectionInfo connectionInfo; + private ITestRuntimeProvider runtimeProvider; + /// /// Initializes a new instance of the class. /// /// Protocol configuration. - /// Transport layer to set up connection - public TestRequestSender(ProtocolConfig protocolConfig, TestHostConnectionInfo connectionInfo) - : this(connectionInfo, JsonDataSerializer.Instance, protocolConfig, ClientProcessExitWaitTimeout) + /// The runtime provider. + public TestRequestSender(ProtocolConfig protocolConfig, ITestRuntimeProvider runtimeProvider) + : this( + runtimeProvider, + communicationEndPoint: null, + runtimeProvider.GetTestHostConnectionInfo(), + JsonDataSerializer.Instance, + protocolConfig, + ClientProcessExitWaitTimeout) { this.SetCommunicationEndPoint(); } internal TestRequestSender( + ITestRuntimeProvider runtimeProvider, + ICommunicationEndPoint communicationEndPoint, TestHostConnectionInfo connectionInfo, IDataSerializer serializer, ProtocolConfig protocolConfig, @@ -79,6 +92,8 @@ internal TestRequestSender( this.highestSupportedVersion = protocolConfig.Version; // The connectionInfo here is that of RuntimeProvider, so reverse the role of runner. + this.runtimeProvider = runtimeProvider; + this.communicationEndpoint = communicationEndPoint; this.connectionInfo.Endpoint = connectionInfo.Endpoint; this.connectionInfo.Role = connectionInfo.Role == ConnectionRole.Host ? ConnectionRole.Client @@ -100,9 +115,14 @@ internal TestRequestSender( IDataSerializer serializer, ProtocolConfig protocolConfig, int clientExitedWaitTime) - : this(connectionInfo, serializer, protocolConfig, clientExitedWaitTime) + : this( + runtimeProvider: null, + communicationEndPoint, + connectionInfo, + serializer, + protocolConfig, + clientExitedWaitTime) { - this.communicationEndpoint = communicationEndPoint; } /// @@ -279,6 +299,30 @@ public void StartTestRun(TestRunCriteriaWithSources runCriteria, ITestRunEventsH this.onMessageReceived = (sender, args) => this.OnExecutionMessageReceived(sender, args, eventHandler); this.channel.MessageReceived += this.onMessageReceived; + // This code section is needed because we altered the old testhost launch process for + // the debugging workflow. Now we don't ask VS to launch and attach to the testhost + // process for us as we previously did, instead we launch it as a standalone process + // and rely on the testhost to ask VS to attach the debugger to itself. + // + // In order to avoid breaking compatibility with previous testhost versions because of + // those changes (older testhosts won't know to request VS to attach to themselves + // thinking instead VS launched and attached to them already), we request VS to attach + // to the testhost here before starting the test run. + if (runCriteria.TestExecutionContext != null + && runCriteria.TestExecutionContext.IsDebug + && this.runtimeProvider is ITestRuntimeProvider2 convertedRuntimeProvider + && this.protocolVersion < ObjectModelConstants.MinimumProtocolVersionWithDebugSupport) + { + var handler = (ITestRunEventsHandler2)eventHandler; + if (!convertedRuntimeProvider.AttachDebuggerToTestHost()) + { + EqtTrace.Warning( + string.Format( + CultureInfo.CurrentUICulture, + CommonResources.AttachDebuggerToDefaultTestHostFailure)); + } + } + var message = this.dataSerializer.SerializePayload( MessageType.StartTestExecutionWithSources, runCriteria, @@ -304,10 +348,35 @@ public void StartTestRun(TestRunCriteriaWithTests runCriteria, ITestRunEventsHan this.onMessageReceived = (sender, args) => this.OnExecutionMessageReceived(sender, args, eventHandler); this.channel.MessageReceived += this.onMessageReceived; + // This code section is needed because we altered the old testhost launch process for + // the debugging workflow. Now we don't ask VS to launch and attach to the testhost + // process for us as we previously did, instead we launch it as a standalone process + // and rely on the testhost to ask VS to attach the debugger to itself. + // + // In order to avoid breaking compatibility with previous testhost versions because of + // those changes (older testhosts won't know to request VS to attach to themselves + // thinking instead VS launched and attached to them already), we request VS to attach + // to the testhost here before starting the test run. + if (runCriteria.TestExecutionContext != null + && runCriteria.TestExecutionContext.IsDebug + && this.runtimeProvider is ITestRuntimeProvider2 convertedRuntimeProvider + && this.protocolVersion < ObjectModelConstants.MinimumProtocolVersionWithDebugSupport) + { + var handler = (ITestRunEventsHandler2)eventHandler; + if (!convertedRuntimeProvider.AttachDebuggerToTestHost()) + { + EqtTrace.Warning( + string.Format( + CultureInfo.CurrentUICulture, + CommonResources.AttachDebuggerToDefaultTestHostFailure)); + } + } + var message = this.dataSerializer.SerializePayload( MessageType.StartTestExecutionWithTests, runCriteria, this.protocolVersion); + if (EqtTrace.IsVerboseEnabled) { EqtTrace.Verbose("TestRequestSender.StartTestRun: Sending test run with message: {0}", message); @@ -452,6 +521,19 @@ private void OnExecutionMessageReceived(object sender, MessageReceivedEventArgs this.channel.Send(data); break; + + case MessageType.AttachDebugger: + var testProcessPid = this.dataSerializer.DeserializePayload(message); + bool result = ((ITestRunEventsHandler2)testRunEventsHandler).AttachDebuggerToProcess(testProcessPid.ProcessID); + + var resultMessage = this.dataSerializer.SerializePayload( + MessageType.AttachDebuggerCallback, + result, + this.protocolVersion); + + this.channel.Send(resultMessage); + + break; } } catch (Exception exception) diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/FrameworkHandle.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/FrameworkHandle.cs index 8aaa8b03bf..ca8ba14780 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/FrameworkHandle.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Adapter/FrameworkHandle.cs @@ -5,7 +5,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Adapter { using System; using System.Collections.Generic; - + using System.Globalization; using Execution; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Adapter /// /// Handle to the framework which is passed to the test executors. /// - internal class FrameworkHandle : TestExecutionRecorder, IFrameworkHandle, IDisposable + internal class FrameworkHandle : TestExecutionRecorder, IFrameworkHandle2, IDisposable { /// /// boolean that gives the value of EnableShutdownAfterTestRun. @@ -110,6 +110,11 @@ public int LaunchProcessWithDebuggerAttached(string filePath, string workingDire return this.testRunEventsHandler.LaunchProcessWithDebuggerAttached(processInfo); } + /// + public bool AttachDebuggerToProcess(int pid) + { + return ((ITestRunEventsHandler2)this.testRunEventsHandler).AttachDebuggerToProcess(pid); + } public void Dispose() { diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunEventsHandler.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunEventsHandler.cs index 4342b9708d..bacf1da410 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunEventsHandler.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/Parallel/ParallelRunEventsHandler.cs @@ -3,9 +3,10 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client.Parallel { + using System; using System.Collections.Generic; using System.Collections.ObjectModel; - + using System.Globalization; using Microsoft.VisualStudio.TestPlatform.Common.Telemetry; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces; @@ -18,7 +19,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client.Parallel /// /// ParallelRunEventsHandler for handling the run events in case of parallel execution /// - internal class ParallelRunEventsHandler : ITestRunEventsHandler + internal class ParallelRunEventsHandler : ITestRunEventsHandler2 { private IProxyExecutionManager proxyExecutionManager; @@ -174,6 +175,12 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta return this.actualRunEventsHandler.LaunchProcessWithDebuggerAttached(testProcessStartInfo); } + /// + public bool AttachDebuggerToProcess(int pid) + { + return ((ITestRunEventsHandler2)this.actualRunEventsHandler).AttachDebuggerToProcess(pid); + } + private void ConvertToRawMessageAndSend(string messageType, object payload) { var rawMessage = this.dataSerializer.SerializePayload(messageType, payload); diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyExecutionManager.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyExecutionManager.cs index 1561608e68..ba157953ef 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyExecutionManager.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyExecutionManager.cs @@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Globalization; using System.Linq; using Microsoft.VisualStudio.TestPlatform.Common; @@ -26,7 +27,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client /// /// Orchestrates test execution operations for the engine communicating with the client. /// - internal class ProxyExecutionManager : ProxyOperationManager, IProxyExecutionManager, ITestRunEventsHandler + internal class ProxyExecutionManager : ProxyOperationManager, IProxyExecutionManager, ITestRunEventsHandler2 { private readonly ITestRuntimeProvider testHostManager; private IDataSerializer dataSerializer; @@ -193,6 +194,12 @@ public virtual int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testPr return this.baseTestRunEventsHandler.LaunchProcessWithDebuggerAttached(testProcessStartInfo); } + /// + public bool AttachDebuggerToProcess(int pid) + { + return ((ITestRunEventsHandler2)this.baseTestRunEventsHandler).AttachDebuggerToProcess(pid); + } + /// /// Aborts the test run. /// diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/DataCollection/DataCollectionTestRunEventsHandler.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/DataCollection/DataCollectionTestRunEventsHandler.cs index 0af3ecd996..8a7025fe82 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/DataCollection/DataCollectionTestRunEventsHandler.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/DataCollection/DataCollectionTestRunEventsHandler.cs @@ -6,6 +6,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.DataCollection using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Globalization; using System.Linq; using System.Threading; @@ -21,7 +22,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.DataCollection /// Handles DataCollection attachments by calling DataCollection Process on Test Run Complete. /// Existing functionality of ITestRunEventsHandler is decorated with additional call to Data Collection Process. /// - internal class DataCollectionTestRunEventsHandler : ITestRunEventsHandler + internal class DataCollectionTestRunEventsHandler : ITestRunEventsHandler2 { private IProxyDataCollectionManager proxyDataCollectionManager; private ITestRunEventsHandler testRunEventsHandler; @@ -157,6 +158,12 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta return this.testRunEventsHandler.LaunchProcessWithDebuggerAttached(testProcessStartInfo); } + /// + public bool AttachDebuggerToProcess(int pid) + { + return ((ITestRunEventsHandler2)this.testRunEventsHandler).AttachDebuggerToProcess(pid); + } + /// /// The get combined attachment sets. /// diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/EventHandlers/TestRequestHandler.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/EventHandlers/TestRequestHandler.cs index 405463032f..5319d0bb78 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/EventHandlers/TestRequestHandler.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/EventHandlers/TestRequestHandler.cs @@ -19,27 +19,27 @@ namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Microsoft.VisualStudio.TestPlatform.Utilities; using CrossPlatResources = Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Resources.Resources; + using ObjectModelConstants = Microsoft.VisualStudio.TestPlatform.ObjectModel.Constants; public class TestRequestHandler : ITestRequestHandler { + private int protocolVersion = 1; + private int highestSupportedVersion = 3; + private readonly IDataSerializer dataSerializer; private ITestHostManagerFactory testHostManagerFactory; private ICommunicationEndPoint communicationEndPoint; private ICommunicationEndpointFactory communicationEndpointFactory; - private int protocolVersion = 1; - - public TestHostConnectionInfo ConnectionInfo { get; set; } - - private int highestSupportedVersion = 2; - private JobQueue jobQueue; private ICommunicationChannel channel; + private JobQueue jobQueue; private ManualResetEventSlim requestSenderConnected; private ManualResetEventSlim testHostManagerFactoryReady; - private ManualResetEventSlim sessionCompleted; + private Action onLaunchAdapterProcessWithDebuggerAttachedAckReceived; + private Action onAttachDebuggerAckRecieved; - private Action onAckMessageRecieved; + public TestHostConnectionInfo ConnectionInfo { get; set; } /// /// Initializes a new instance of the . @@ -48,7 +48,13 @@ public class TestRequestHandler : ITestRequestHandler { } - protected TestRequestHandler(TestHostConnectionInfo connectionInfo, ICommunicationEndpointFactory communicationEndpointFactory, IDataSerializer dataSerializer, JobQueue jobQueue, Action onAckMessageRecieved) + protected TestRequestHandler( + TestHostConnectionInfo connectionInfo, + ICommunicationEndpointFactory communicationEndpointFactory, + IDataSerializer dataSerializer, + JobQueue jobQueue, + Action onLaunchAdapterProcessWithDebuggerAttachedAckReceived, + Action onAttachDebuggerAckRecieved) { this.communicationEndpointFactory = communicationEndpointFactory; this.ConnectionInfo = connectionInfo; @@ -56,7 +62,8 @@ protected TestRequestHandler(TestHostConnectionInfo connectionInfo, ICommunicati this.requestSenderConnected = new ManualResetEventSlim(false); this.testHostManagerFactoryReady = new ManualResetEventSlim(false); this.sessionCompleted = new ManualResetEventSlim(false); - this.onAckMessageRecieved = onAckMessageRecieved; + this.onLaunchAdapterProcessWithDebuggerAttachedAckReceived = onLaunchAdapterProcessWithDebuggerAttachedAckReceived; + this.onAttachDebuggerAckRecieved = onAttachDebuggerAckRecieved; this.jobQueue = jobQueue; } @@ -67,7 +74,8 @@ protected TestRequestHandler(IDataSerializer dataSerializer, ICommunicationEndpo this.requestSenderConnected = new ManualResetEventSlim(false); this.sessionCompleted = new ManualResetEventSlim(false); this.testHostManagerFactoryReady = new ManualResetEventSlim(false); - this.onAckMessageRecieved = (message) => { throw new NotImplementedException(); }; + this.onLaunchAdapterProcessWithDebuggerAttachedAckReceived = (message) => { throw new NotImplementedException(); }; + this.onAttachDebuggerAckRecieved = (message) => { throw new NotImplementedException(); }; this.jobQueue = new JobQueue( (action) => { action(); }, @@ -190,7 +198,7 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta { var waitHandle = new ManualResetEventSlim(false); Message ackMessage = null; - this.onAckMessageRecieved = (ackRawMessage) => + this.onLaunchAdapterProcessWithDebuggerAttachedAckReceived = (ackRawMessage) => { ackMessage = ackRawMessage; waitHandle.Set(); @@ -203,10 +211,43 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta EqtTrace.Verbose("Waiting for LaunchAdapterProcessWithDebuggerAttached ack"); waitHandle.Wait(); - this.onAckMessageRecieved = null; + this.onLaunchAdapterProcessWithDebuggerAttachedAckReceived = null; return this.dataSerializer.DeserializePayload(ackMessage); } + /// + public bool AttachDebuggerToProcess(int pid) + { + // 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.protocolVersion < ObjectModelConstants.MinimumProtocolVersionWithDebugSupport) + { + return false; + } + + Message ackMessage = null; + var waitHandle = new ManualResetEventSlim(false); + + this.onAttachDebuggerAckRecieved = (ackRawMessage) => + { + ackMessage = ackRawMessage; + waitHandle.Set(); + }; + + var data = dataSerializer.SerializePayload( + MessageType.AttachDebugger, + new TestProcessAttachDebuggerPayload(pid), + protocolVersion); + this.SendData(data); + + EqtTrace.Verbose("Waiting for AttachDebuggerToProcess ack ..."); + waitHandle.Wait(); + + this.onAttachDebuggerAckRecieved = null; + return this.dataSerializer.DeserializePayload(ackMessage); + } + public void OnMessageReceived(object sender, MessageReceivedEventArgs messageReceivedArgs) { var message = this.dataSerializer.DeserializeMessage(messageReceivedArgs.Data); @@ -325,7 +366,11 @@ public void OnMessageReceived(object sender, MessageReceivedEventArgs messageRec break; case MessageType.LaunchAdapterProcessWithDebuggerAttachedCallback: - this.onAckMessageRecieved?.Invoke(message); + this.onLaunchAdapterProcessWithDebuggerAttachedAckReceived?.Invoke(message); + break; + + case MessageType.AttachDebuggerCallback: + this.onAttachDebuggerAckRecieved?.Invoke(message); break; case MessageType.AbortTestRun: diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs index ab438113dc..7539321f54 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs @@ -282,9 +282,30 @@ internal void Cancel() protected abstract void BeforeRaisingTestRunComplete(bool exceptionsHitDuringRunTests); - protected abstract IEnumerable> GetExecutorUriExtensionMap(IFrameworkHandle testExecutorFrameworkHandle, RunContext runContext); + protected abstract IEnumerable> GetExecutorUriExtensionMap( + IFrameworkHandle testExecutorFrameworkHandle, + RunContext runContext); - protected abstract void InvokeExecutor(LazyExtension executor, Tuple executorUriExtensionTuple, RunContext runContext, IFrameworkHandle frameworkHandle); + protected abstract void InvokeExecutor( + LazyExtension executor, + Tuple executorUriExtensionTuple, + RunContext runContext, + IFrameworkHandle frameworkHandle); + + /// + /// Asks the adapter about attaching the debugger to the default test host. + /// + /// The executor used to run the tests. + /// The executor URI. + /// The run context. + /// + /// if must attach the debugger to the default test host, + /// otherwise. + /// + protected abstract bool ShouldAttachDebuggerToTestHost( + LazyExtension executor, + Tuple executorUriExtensionTuple, + RunContext runContext); protected abstract void SendSessionStart(); @@ -363,122 +384,174 @@ private TimeSpan RunTestsInternal() [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This methods must call all possible executors and not fail on crash in any executor.")] private bool RunTestInternalWithExecutors(IEnumerable> executorUriExtensionMap, long totalTests) { - double totalTimeTakenByAdapters = 0; - - var executorsFromDeprecatedLocations = false; - - // Call the executor for each group of tests. - var exceptionsHitDuringRunTests = false; - - // Collecting Total Number of Adapters Discovered in Machine + // Collecting Total Number of Adapters Discovered in Machine. this.requestData.MetricsCollection.Add(TelemetryDataConstants.NumberOfAdapterDiscoveredDuringExecution, executorUriExtensionMap.Count()); + var attachedToTestHost = false; + var executorCache = new Dictionary>(); foreach (var executorUriExtensionTuple in executorUriExtensionMap) { - // Get the executor + // Avoid processing the same executor twice. + if (executorCache.ContainsKey(executorUriExtensionTuple.Item1.AbsoluteUri)) + { + continue; + } + + // Get the extension manager. var extensionManager = this.GetExecutorExtensionManager(executorUriExtensionTuple.Item2); // Look up the executor. var executor = extensionManager.TryGetTestExtension(executorUriExtensionTuple.Item1); - if (executor != null) + if (executor == null) { - try - { - if (EqtTrace.IsVerboseEnabled) - { - EqtTrace.Verbose( - "BaseRunTests.RunTestInternalWithExecutors: Running tests for {0}", - executor.Metadata.ExtensionUri); - } - - // set the active executor - this.activeExecutor = executor.Value; + // Commenting this out because of a compatibility issue with Microsoft.Dotnet.ProjectModel released on nuGet.org. + // this.activeExecutor = null; + // var runtimeVersion = string.Concat(PlatformServices.Default.Runtime.RuntimeType, " ", + // PlatformServices.Default.Runtime.RuntimeVersion); + var runtimeVersion = " "; + this.TestRunEventsHandler?.HandleLogMessage( + TestMessageLevel.Warning, + string.Format( + CultureInfo.CurrentUICulture, + CrossPlatEngineResources.NoMatchingExecutor, + executorUriExtensionTuple.Item1.AbsoluteUri, + runtimeVersion)); - // If test run cancellation is requested, skip the next executor - if (this.isCancellationRequested) - { - break; - } + continue; + } - var timeStartNow = DateTime.UtcNow; + // Cache the executor. + executorCache.Add(executorUriExtensionTuple.Item1.AbsoluteUri, executor); - var currentTotalTests = this.testRunCache.TotalExecutedTests; - this.testPlatformEventSource.AdapterExecutionStart(executorUriExtensionTuple.Item1.AbsoluteUri); + // Check if we actually have to attach to the default test host. + if (!this.runContext.IsBeingDebugged || attachedToTestHost) + { + // We already know we should attach to the default test host, simply continue. + continue; + } - // Run the tests. - if (this.NotRequiredSTAThread() || !this.TryToRunInSTAThread(() => this.InvokeExecutor(executor, executorUriExtensionTuple, this.runContext, this.frameworkHandle), true)) - { - this.InvokeExecutor(executor, executorUriExtensionTuple, this.runContext, this.frameworkHandle); - } + // If there's at least one adapter in the filtered adapters list that doesn't + // implement the new test executor interface, we should attach to the default test + // host by default. + // Same goes if all adapters implement the new test executor interface but at + // least one of them needs the test platform to attach to the default test host. + if (!(executor.Value is ITestExecutor2) + || this.ShouldAttachDebuggerToTestHost(executor, executorUriExtensionTuple, this.runContext)) + { + EqtTrace.Verbose("Attaching to default test host."); - this.testPlatformEventSource.AdapterExecutionStop(this.testRunCache.TotalExecutedTests - currentTotalTests); + attachedToTestHost = true; + var pid = Process.GetCurrentProcess().Id; + if (!this.frameworkHandle.AttachDebuggerToProcess(pid)) + { + EqtTrace.Warning( + string.Format( + CultureInfo.CurrentUICulture, + CrossPlatEngineResources.AttachDebuggerToDefaultTestHostFailure, + pid)); + } + } + } - var totalTimeTaken = DateTime.UtcNow - timeStartNow; - // Identify whether the executor did run any tests at all - if (this.testRunCache.TotalExecutedTests > totalTests) - { - this.executorUrisThatRanTests.Add(executorUriExtensionTuple.Item1.AbsoluteUri); + // Call the executor for each group of tests. + var exceptionsHitDuringRunTests = false; + var executorsFromDeprecatedLocations = false; + double totalTimeTakenByAdapters = 0; + foreach (var executorUriExtensionTuple in executorUriExtensionMap) + { + // Get the executor from the cache. + if (!executorCache.TryGetValue(executorUriExtensionTuple.Item1.AbsoluteUri, out var executor)) + { + continue; + } - // Collecting Total Tests Ran by each Adapter - var totalTestRun = this.testRunCache.TotalExecutedTests - totalTests; - this.requestData.MetricsCollection.Add(string.Format("{0}.{1}", TelemetryDataConstants.TotalTestsRanByAdapter, executorUriExtensionTuple.Item1.AbsoluteUri), totalTestRun); + try + { + if (EqtTrace.IsVerboseEnabled) + { + EqtTrace.Verbose( + "BaseRunTests.RunTestInternalWithExecutors: Running tests for {0}", + executor.Metadata.ExtensionUri); + } - if (!CrossPlatEngine.Constants.DefaultAdapters.Contains(executor.Metadata.ExtensionUri, StringComparer.OrdinalIgnoreCase)) - { - var executorLocation = executor.Value.GetType().GetTypeInfo().Assembly.GetAssemblyLocation(); + // set the active executor + this.activeExecutor = executor.Value; - executorsFromDeprecatedLocations |= Path.GetDirectoryName(executorLocation).Equals(CrossPlatEngine.Constants.DefaultAdapterLocation); - } + // If test run cancellation is requested, skip the next executor + if (this.isCancellationRequested) + { + break; + } - totalTests = this.testRunCache.TotalExecutedTests; - } + var timeStartNow = DateTime.UtcNow; - if (EqtTrace.IsVerboseEnabled) - { - EqtTrace.Verbose( - "BaseRunTests.RunTestInternalWithExecutors: Completed running tests for {0}", - executor.Metadata.ExtensionUri); - } + var currentTotalTests = this.testRunCache.TotalExecutedTests; + this.testPlatformEventSource.AdapterExecutionStart(executorUriExtensionTuple.Item1.AbsoluteUri); - // Collecting Time Taken by each executor Uri - this.requestData.MetricsCollection.Add(string.Format("{0}.{1}", TelemetryDataConstants.TimeTakenToRunTestsByAnAdapter, executorUriExtensionTuple.Item1.AbsoluteUri), totalTimeTaken.TotalSeconds); - totalTimeTakenByAdapters += totalTimeTaken.TotalSeconds; + // Run the tests. + if (this.NotRequiredSTAThread() || !this.TryToRunInSTAThread(() => this.InvokeExecutor(executor, executorUriExtensionTuple, this.runContext, this.frameworkHandle), true)) + { + this.InvokeExecutor(executor, executorUriExtensionTuple, this.runContext, this.frameworkHandle); } - catch (Exception e) + + this.testPlatformEventSource.AdapterExecutionStop(this.testRunCache.TotalExecutedTests - currentTotalTests); + + var totalTimeTaken = DateTime.UtcNow - timeStartNow; + + // Identify whether the executor did run any tests at all + if (this.testRunCache.TotalExecutedTests > totalTests) { - exceptionsHitDuringRunTests = true; + this.executorUrisThatRanTests.Add(executorUriExtensionTuple.Item1.AbsoluteUri); - if (EqtTrace.IsErrorEnabled) + // Collecting Total Tests Ran by each Adapter + var totalTestRun = this.testRunCache.TotalExecutedTests - totalTests; + this.requestData.MetricsCollection.Add(string.Format("{0}.{1}", TelemetryDataConstants.TotalTestsRanByAdapter, executorUriExtensionTuple.Item1.AbsoluteUri), totalTestRun); + + if (!CrossPlatEngine.Constants.DefaultAdapters.Contains(executor.Metadata.ExtensionUri, StringComparer.OrdinalIgnoreCase)) { - EqtTrace.Error( - "BaseRunTests.RunTestInternalWithExecutors: An exception occurred while invoking executor {0}. {1}.", - executorUriExtensionTuple.Item1, - e); + var executorLocation = executor.Value.GetType().GetTypeInfo().Assembly.GetAssemblyLocation(); + + executorsFromDeprecatedLocations |= Path.GetDirectoryName(executorLocation).Equals(CrossPlatEngine.Constants.DefaultAdapterLocation); } - this.TestRunEventsHandler?.HandleLogMessage( - TestMessageLevel.Error, - string.Format( - CultureInfo.CurrentCulture, - CrossPlatEngineResources.ExceptionFromRunTests, - executorUriExtensionTuple.Item1, - ExceptionUtilities.GetExceptionMessage(e))); + totalTests = this.testRunCache.TotalExecutedTests; } - finally + + if (EqtTrace.IsVerboseEnabled) { - this.activeExecutor = null; + EqtTrace.Verbose( + "BaseRunTests.RunTestInternalWithExecutors: Completed running tests for {0}", + executor.Metadata.ExtensionUri); } + + // Collecting Time Taken by each executor Uri + this.requestData.MetricsCollection.Add(string.Format("{0}.{1}", TelemetryDataConstants.TimeTakenToRunTestsByAnAdapter, executorUriExtensionTuple.Item1.AbsoluteUri), totalTimeTaken.TotalSeconds); + totalTimeTakenByAdapters += totalTimeTaken.TotalSeconds; } - else + catch (Exception e) { - // Commenting this out because of a compatibility issue with Microsoft.Dotnet.ProjectModel released on nuGet.org. - // var runtimeVersion = string.Concat(PlatformServices.Default.Runtime.RuntimeType, " ", - // PlatformServices.Default.Runtime.RuntimeVersion); - var runtimeVersion = " "; + exceptionsHitDuringRunTests = true; + + if (EqtTrace.IsErrorEnabled) + { + EqtTrace.Error( + "BaseRunTests.RunTestInternalWithExecutors: An exception occurred while invoking executor {0}. {1}.", + executorUriExtensionTuple.Item1, + e); + } + this.TestRunEventsHandler?.HandleLogMessage( - TestMessageLevel.Warning, - string.Format(CultureInfo.CurrentUICulture, CrossPlatEngineResources.NoMatchingExecutor, executorUriExtensionTuple.Item1, runtimeVersion)); + TestMessageLevel.Error, + string.Format( + CultureInfo.CurrentCulture, + CrossPlatEngineResources.ExceptionFromRunTests, + executorUriExtensionTuple.Item1, + ExceptionUtilities.GetExceptionMessage(e))); + } + finally + { + this.activeExecutor = null; } } diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/RunTestsWithSources.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/RunTestsWithSources.cs index be0335abc8..377cac355c 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/RunTestsWithSources.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/RunTestsWithSources.cs @@ -111,11 +111,33 @@ protected override IEnumerable> GetExecutorUriExtensionMap(IFr return executorUris; } - protected override void InvokeExecutor(LazyExtension executor, Tuple executorUriExtensionTuple, RunContext runContext, IFrameworkHandle frameworkHandle) + protected override void InvokeExecutor( + LazyExtension executor, + Tuple executorUriExtensionTuple, + RunContext runContext, + IFrameworkHandle frameworkHandle) { executor?.Value.RunTests(this.executorUriVsSourceList[executorUriExtensionTuple], runContext, frameworkHandle); } + /// + protected override bool ShouldAttachDebuggerToTestHost( + LazyExtension executor, + Tuple executorUriExtensionTuple, + RunContext runContext) + { + // If the adapter doesn't implement the new test executor interface we should attach to + // the default test host by default to preserve old behavior. + if (!(executor?.Value is ITestExecutor2 convertedExecutor)) + { + return true; + } + + return convertedExecutor.ShouldAttachToTestHost( + this.executorUriVsSourceList[executorUriExtensionTuple], + runContext); + } + /// /// Returns executor Vs sources list /// diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/RunTestsWithTests.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/RunTestsWithTests.cs index 52f32507ce..79990b4b19 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/RunTestsWithTests.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/RunTestsWithTests.cs @@ -12,11 +12,11 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Execution using Microsoft.VisualStudio.TestPlatform.CoreUtilities.Tracing; using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Adapter; using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Utilities; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine.ClientProtocol; - using ObjectModel; using ObjectModel.Client; internal class RunTestsWithTests : BaseRunTests @@ -66,11 +66,31 @@ protected override IEnumerable> GetExecutorUriExtensionMap(IF return this.executorUriVsTestList.Keys; } - protected override void InvokeExecutor(LazyExtension executor, Tuple executorUri, RunContext runContext, IFrameworkHandle frameworkHandle) + protected override void InvokeExecutor( + LazyExtension executor, + Tuple executorUri, + RunContext runContext, + IFrameworkHandle frameworkHandle) { executor?.Value.RunTests(this.executorUriVsTestList[executorUri], runContext, frameworkHandle); } + /// + protected override bool ShouldAttachDebuggerToTestHost( + LazyExtension executor, + Tuple executorUri, + RunContext runContext) + { + // If the adapter doesn't implement the new test executor interface we should attach to + // the default test host by default to preserve old behavior. + if (!(executor?.Value is ITestExecutor2 convertedExecutor)) + { + return true; + } + + return convertedExecutor.ShouldAttachToTestHost(this.executorUriVsTestList[executorUri], runContext); + } + /// /// Sends Session-End event on in-proc datacollectors /// diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/Resources.Designer.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/Resources.Designer.cs index 29ba479681..76c0fcc402 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/Resources.Designer.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/Resources.Designer.cs @@ -10,7 +10,6 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Resources { using System; - using System.Reflection; /// @@ -20,7 +19,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -40,7 +39,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Resources.Resources", typeof(Resources).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Resources.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -61,6 +60,15 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to Cannot attach the debugger to the default test host with process ID: {0}.. + /// + internal static string AttachDebuggerToDefaultTestHostFailure { + get { + return ResourceManager.GetString("AttachDebuggerToDefaultTestHostFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to DataCollector debugging is enabled. Please attach debugger to datacollector process to continue.. /// diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/Resources.resx b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/Resources.resx index 8e51734589..021500345d 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/Resources.resx +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/Resources.resx @@ -198,4 +198,7 @@ Discovery of tests cancelled. + + Cannot attach the debugger to the default test host with process ID: {0}. + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.cs.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.cs.xlf index 14f494a338..e6c8dec431 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.cs.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.cs.xlf @@ -207,6 +207,11 @@ Zjišťování testů bylo zrušeno. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.de.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.de.xlf index 9db63b78a6..bf5dbceca2 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.de.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.de.xlf @@ -207,6 +207,11 @@ Die Ermittlung von Tests wurde abgebrochen. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.es.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.es.xlf index 555d8e6df6..ed13906318 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.es.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.es.xlf @@ -207,6 +207,11 @@ Se ha cancelado la detección de las pruebas. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.fr.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.fr.xlf index c9c20f5c50..01b56d16a4 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.fr.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.fr.xlf @@ -207,6 +207,11 @@ Découverte de tests annulée. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.it.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.it.xlf index d1db7c13bf..d0e4801854 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.it.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.it.xlf @@ -207,6 +207,11 @@ Individuazione dei test annullata. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ja.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ja.xlf index fa56879978..e036370c89 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ja.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ja.xlf @@ -207,6 +207,11 @@ テストの検出が取り消されました。 + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ko.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ko.xlf index a8f527ff04..138614b854 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ko.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ko.xlf @@ -207,6 +207,11 @@ 테스트 검색이 취소되었습니다. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.pl.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.pl.xlf index c7edd75ec9..749f3cc4a5 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.pl.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.pl.xlf @@ -207,6 +207,11 @@ Odnajdywanie testów zostało anulowane. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.pt-BR.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.pt-BR.xlf index a647b1b972..44da41742a 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.pt-BR.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.pt-BR.xlf @@ -207,6 +207,11 @@ Descoberta de testes cancelada. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ru.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ru.xlf index 75dcfb6b95..0ebcb9e5dd 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ru.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.ru.xlf @@ -207,6 +207,11 @@ Обнаружение тестов отменено. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.tr.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.tr.xlf index b1a2a88211..924a516fb4 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.tr.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.tr.xlf @@ -207,6 +207,11 @@ Test bulma iptal edildi. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.xlf index 087be26de8..453f023f7f 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.xlf @@ -118,6 +118,11 @@ Discovery of tests cancelled. + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.zh-Hans.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.zh-Hans.xlf index 6799925541..e544a4d368 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.zh-Hans.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.zh-Hans.xlf @@ -207,6 +207,11 @@ 已取消测试发现。 + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.zh-Hant.xlf b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.zh-Hant.xlf index b6cff255d6..88ef56f6b7 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.zh-Hant.xlf +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Resources/xlf/Resources.zh-Hant.xlf @@ -207,6 +207,11 @@ 已取消測試的探索。 + + Cannot attach the debugger to the default test host with process ID: {0}. + Cannot attach the debugger to the default test host with process ID: {0}. + + \ No newline at end of file diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/TestEngine.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/TestEngine.cs index 4beb28b714..fa6364b08f 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/TestEngine.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/TestEngine.cs @@ -85,7 +85,7 @@ public IProxyDiscoveryManager GetDiscoveryManager(IRequestData requestData, ITes var hostManager = this.testHostProviderManager.GetTestHostManagerByRunConfiguration(discoveryCriteria.RunSettings); hostManager?.Initialize(TestSessionMessageLogger.Instance, discoveryCriteria.RunSettings); - return new ProxyDiscoveryManager(requestData, new TestRequestSender(requestData.ProtocolConfig, hostManager.GetTestHostConnectionInfo()), hostManager); + return new ProxyDiscoveryManager(requestData, new TestRequestSender(requestData.ProtocolConfig, hostManager), hostManager); }; return !testHostManager.Shared ? new ParallelProxyDiscoveryManager(requestData, proxyDiscoveryManagerCreator, parallelLevel, sharedHosts: testHostManager.Shared) : proxyDiscoveryManagerCreator(); @@ -131,7 +131,7 @@ public IProxyExecutionManager GetExecutionManager(IRequestData requestData, ITes hostManager.SetCustomLauncher(testRunCriteria.TestHostLauncher); } - var requestSender = new TestRequestSender(requestData.ProtocolConfig, hostManager.GetTestHostConnectionInfo()); + var requestSender = new TestRequestSender(requestData.ProtocolConfig, hostManager); return isDataCollectorEnabled ? new ProxyExecutionManagerWithDataCollection(requestData, requestSender, hostManager, new ProxyDataCollectionManager(requestData, testRunCriteria.TestRunSettings, GetSourcesFromTestRunCriteria(testRunCriteria))) : new ProxyExecutionManager(requestData, requestSender, hostManager); diff --git a/src/Microsoft.TestPlatform.ObjectModel/Adapter/Interfaces/IFrameworkHandle2.cs b/src/Microsoft.TestPlatform.ObjectModel/Adapter/Interfaces/IFrameworkHandle2.cs new file mode 100644 index 0000000000..9f656b590c --- /dev/null +++ b/src/Microsoft.TestPlatform.ObjectModel/Adapter/Interfaces/IFrameworkHandle2.cs @@ -0,0 +1,18 @@ +// 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.ObjectModel.Adapter +{ + /// + /// Handle to the framework which is passed to the test executors. + /// + public interface IFrameworkHandle2 : IFrameworkHandle + { + /// + /// Attach debugger to an already running process. + /// + /// Process ID of the process to which the debugger should be attached. + /// if the debugger was successfully attached to the requested process, otherwise. + bool AttachDebuggerToProcess(int pid); + } +} diff --git a/src/Microsoft.TestPlatform.ObjectModel/Adapter/Interfaces/ITestExecutor2.cs b/src/Microsoft.TestPlatform.ObjectModel/Adapter/Interfaces/ITestExecutor2.cs new file mode 100644 index 0000000000..2f3ac9b402 --- /dev/null +++ b/src/Microsoft.TestPlatform.ObjectModel/Adapter/Interfaces/ITestExecutor2.cs @@ -0,0 +1,40 @@ +// 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.ObjectModel.Adapter +{ + using System; + using System.Collections.Generic; + + /// + /// Defines the test executor which provides capability to run tests. + /// + /// A class that implements this interface will be available for use if its containing + // assembly is either placed in the Extensions folder or is marked as a 'UnitTestExtension' type + // in the vsix package. + /// + public interface ITestExecutor2 : ITestExecutor + { + /// + /// Indicates whether or not the default test host process should be attached to. + /// + /// Path to test container files to look for tests in. + /// Context to use when executing the tests. + /// + /// if the default test host process should be attached to, + /// otherwise. + /// + bool ShouldAttachToTestHost(IEnumerable sources, IRunContext runContext); + + /// + /// Indicates whether or not the default test host process should be attached to. + /// + /// Tests to be run. + /// Context to use when executing the tests. + /// + /// if the default test host process should be attached to, + /// otherwise. + /// + bool ShouldAttachToTestHost(IEnumerable tests, IRunContext runContext); + } +} diff --git a/src/Microsoft.TestPlatform.ObjectModel/Client/Interfaces/ITestHostLauncher2.cs b/src/Microsoft.TestPlatform.ObjectModel/Client/Interfaces/ITestHostLauncher2.cs new file mode 100644 index 0000000000..641c6cbba7 --- /dev/null +++ b/src/Microsoft.TestPlatform.ObjectModel/Client/Interfaces/ITestHostLauncher2.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; + +namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces +{ + /// + /// Interface defining contract for custom test host implementations + /// + public interface ITestHostLauncher2 : ITestHostLauncher + { + /// + /// Attach debugger to already running custom test host process. + /// + /// Process ID of the process to which the debugger should be attached. + /// if the debugger was successfully attached to the requested process, otherwise. + bool AttachDebuggerToProcess(int pid); + + /// + /// Attach debugger to already running custom test host process. + /// + /// Process ID of the process to which the debugger should be attached. + /// The cancellation token. + /// if the debugger was successfully attached to the requested process, otherwise. + bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.TestPlatform.ObjectModel/Client/Interfaces/ITestRunEventsHandler2.cs b/src/Microsoft.TestPlatform.ObjectModel/Client/Interfaces/ITestRunEventsHandler2.cs new file mode 100644 index 0000000000..a8ff3e742f --- /dev/null +++ b/src/Microsoft.TestPlatform.ObjectModel/Client/Interfaces/ITestRunEventsHandler2.cs @@ -0,0 +1,18 @@ +// 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.ObjectModel.Client +{ + /// + /// Interface contract for handling test run events during run operation. + /// + public interface ITestRunEventsHandler2 : ITestRunEventsHandler + { + /// + /// Attach debugger to an already running process. + /// + /// Process ID of the process to which the debugger should be attached. + /// if the debugger was successfully attached to the requested process, otherwise. + bool AttachDebuggerToProcess(int pid); + } +} diff --git a/src/Microsoft.TestPlatform.ObjectModel/Client/Payloads/EditorAttachDebuggerAckPayload.cs b/src/Microsoft.TestPlatform.ObjectModel/Client/Payloads/EditorAttachDebuggerAckPayload.cs new file mode 100644 index 0000000000..268ccd1d6b --- /dev/null +++ b/src/Microsoft.TestPlatform.ObjectModel/Client/Payloads/EditorAttachDebuggerAckPayload.cs @@ -0,0 +1,26 @@ +// 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.ObjectModel.Client +{ + using System.Runtime.Serialization; + + /// + /// Class used to define the sent by the + /// vstest.console translation layers into design mode. + /// + public class EditorAttachDebuggerAckPayload + { + /// + /// A value indicating if the debugger has successfully attached. + /// + [DataMember] + public bool Attached { get; set; } + + /// + /// ErrorMessage, in cases where attaching the debugger fails. + /// + [DataMember] + public string ErrorMessage { get; set; } + } +} diff --git a/src/Microsoft.TestPlatform.ObjectModel/Constants.cs b/src/Microsoft.TestPlatform.ObjectModel/Constants.cs index 0d6da5d243..d3aee31966 100644 --- a/src/Microsoft.TestPlatform.ObjectModel/Constants.cs +++ b/src/Microsoft.TestPlatform.ObjectModel/Constants.cs @@ -168,7 +168,12 @@ public static class Constants /// /// The default protocol version /// - public static readonly ProtocolConfig DefaultProtocolConfig = new ProtocolConfig { Version = 2 }; + public static readonly ProtocolConfig DefaultProtocolConfig = new ProtocolConfig { Version = 3 }; + + /// + /// The minimum protocol version that has debug support + /// + public const int MinimumProtocolVersionWithDebugSupport = 3; /// /// Name of the results directory diff --git a/src/Microsoft.TestPlatform.ObjectModel/Host/ITestRuntimeProvider2.cs b/src/Microsoft.TestPlatform.ObjectModel/Host/ITestRuntimeProvider2.cs new file mode 100644 index 0000000000..170c1e8343 --- /dev/null +++ b/src/Microsoft.TestPlatform.ObjectModel/Host/ITestRuntimeProvider2.cs @@ -0,0 +1,21 @@ +// 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.ObjectModel.Host +{ + /// + /// Interface to define a TestRuntimeProvider with support for attaching the debugger to the + /// default testhost process. + /// + public interface ITestRuntimeProvider2 : ITestRuntimeProvider + { + /// + /// Attach the debugger to an already running testhost process. + /// + /// + /// if the debugger was successfully attached to the running testhost + /// process, otherwise. + /// + bool AttachDebuggerToTestHost(); + } +} diff --git a/src/Microsoft.TestPlatform.ObjectModel/TestProcessAttachDebuggerPayload.cs b/src/Microsoft.TestPlatform.ObjectModel/TestProcessAttachDebuggerPayload.cs new file mode 100644 index 0000000000..371a0dd65f --- /dev/null +++ b/src/Microsoft.TestPlatform.ObjectModel/TestProcessAttachDebuggerPayload.cs @@ -0,0 +1,29 @@ +// 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.ObjectModel +{ + using System.Runtime.Serialization; + + /// + /// The test process info payload. + /// + [DataContract] + public class TestProcessAttachDebuggerPayload + { + /// + /// Creates a new instance of this class. + /// + /// The process id the debugger should attach to. + public TestProcessAttachDebuggerPayload(int pid) + { + this.ProcessID = pid; + } + + /// + /// The process id the debugger should attach to. + /// + [DataMember] + public int ProcessID { get; set; } + } +} diff --git a/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DefaultTestHostManager.cs b/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DefaultTestHostManager.cs index 36016d8ea2..eec25101ae 100644 --- a/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DefaultTestHostManager.cs +++ b/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DefaultTestHostManager.cs @@ -37,7 +37,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Hosting /// [ExtensionUri(DefaultTestHostUri)] [FriendlyName(DefaultTestHostFriendlyName)] - public class DefaultTestHostManager : ITestRuntimeProvider + public class DefaultTestHostManager : ITestRuntimeProvider2 { private const string X64TestHostProcessName = "testhost.exe"; private const string X86TestHostProcessName = "testhost.x86.exe"; @@ -262,6 +262,14 @@ public Task CleanTestHostAsync(CancellationToken cancellationToken) return Task.FromResult(true); } + /// + public bool AttachDebuggerToTestHost() + { + return this.customTestHostLauncher is ITestHostLauncher2 launcher + ? launcher.AttachDebuggerToProcess(this.testHostProcess.Id) + : false; + } + /// /// Filter duplicate extensions, include only the highest versioned extension /// @@ -365,10 +373,18 @@ private bool LaunchHost(TestProcessStartInfo testHostStartInfo, CancellationToke this.testHostProcessStdError = new StringBuilder(0, CoreUtilities.Constants.StandardErrorMaxLength); EqtTrace.Verbose("Launching default test Host Process {0} with arguments {1}", testHostStartInfo.FileName, testHostStartInfo.Arguments); - if (this.customTestHostLauncher == null) + // We launch the test host process here if we're on the normal test running workflow. + // If we're debugging and we have access to the newest version of the testhost launcher + // interface we launch it here as well, but we expect to attach later to the test host + // process by using its PID. + // For every other workflow (e.g.: profiling) we ask the IDE to launch the custom test + // host for us. In the profiling case this is needed because then the IDE sets some + // additional environmental variables for us to help with probing. + if ((this.customTestHostLauncher == null) + || (this.customTestHostLauncher.IsDebug + && this.customTestHostLauncher is ITestHostLauncher2)) { EqtTrace.Verbose("DefaultTestHostManager: Starting process '{0}' with command line '{1}'", testHostStartInfo.FileName, testHostStartInfo.Arguments); - cancellationToken.ThrowIfCancellationRequested(); this.testHostProcess = this.processHelper.LaunchProcess(testHostStartInfo.FileName, testHostStartInfo.Arguments, testHostStartInfo.WorkingDirectory, testHostStartInfo.EnvironmentVariables, this.ErrorReceivedCallback, this.ExitCallBack, null) as Process; } diff --git a/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs b/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs index 324e40be31..514a030ab9 100644 --- a/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs +++ b/src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs @@ -40,7 +40,7 @@ namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Hosting /// [ExtensionUri(DotnetTestHostUri)] [FriendlyName(DotnetTestHostFriendlyName)] - public class DotnetTestHostManager : ITestRuntimeProvider + public class DotnetTestHostManager : ITestRuntimeProvider2 { private const string DotnetTestHostUri = "HostProvider://DotnetTestHost"; private const string DotnetTestHostFriendlyName = "DotnetTestHost"; @@ -371,6 +371,14 @@ public Task CleanTestHostAsync(CancellationToken cancellationToken) return Task.FromResult(true); } + /// + public bool AttachDebuggerToTestHost() + { + return this.customTestHostLauncher is ITestHostLauncher2 launcher + ? launcher.AttachDebuggerToProcess(this.testHostProcess.Id) + : false; + } + /// /// Raises HostLaunched event /// @@ -401,7 +409,17 @@ private void OnHostExited(HostProviderEventArgs e) private bool LaunchHost(TestProcessStartInfo testHostStartInfo, CancellationToken cancellationToken) { this.testHostProcessStdError = new StringBuilder(0, CoreUtilities.Constants.StandardErrorMaxLength); - if (this.customTestHostLauncher == null) + + // We launch the test host process here if we're on the normal test running workflow. + // If we're debugging and we have access to the newest version of the testhost launcher + // interface we launch it here as well, but we expect to attach later to the test host + // process by using its PID. + // For every other workflow (e.g.: profiling) we ask the IDE to launch the custom test + // host for us. In the profiling case this is needed because then the IDE sets some + // additional environmental variables for us to help with probing. + if ((this.customTestHostLauncher == null) + || (this.customTestHostLauncher.IsDebug + && this.customTestHostLauncher is ITestHostLauncher2)) { EqtTrace.Verbose("DotnetTestHostManager: Starting process '{0}' with command line '{1}'", testHostStartInfo.FileName, testHostStartInfo.Arguments); diff --git a/src/Microsoft.TestPlatform.VsTestConsole.TranslationLayer/VsTestConsoleRequestSender.cs b/src/Microsoft.TestPlatform.VsTestConsole.TranslationLayer/VsTestConsoleRequestSender.cs index c5053dbf7a..838cbdcb4b 100644 --- a/src/Microsoft.TestPlatform.VsTestConsole.TranslationLayer/VsTestConsoleRequestSender.cs +++ b/src/Microsoft.TestPlatform.VsTestConsole.TranslationLayer/VsTestConsoleRequestSender.cs @@ -37,7 +37,7 @@ internal class VsTestConsoleRequestSender : ITranslationLayerRequestSender, ITra private bool handShakeSuccessful = false; - private int protocolVersion = 2; + private int protocolVersion = 3; /// /// Use to cancel blocking tasks associated with vstest.console process @@ -636,6 +636,10 @@ private void SendMessageAndListenAndReportTestResults(string messageType, object { HandleCustomHostLaunch(customHostLauncher, message); } + else if (string.Equals(MessageType.EditorAttachDebugger, message.MessageType)) + { + AttachDebuggerToProcess(customHostLauncher, message); + } } } catch (Exception exception) @@ -699,6 +703,10 @@ private async Task SendMessageAndListenAndReportTestResultsAsync(string messageT { HandleCustomHostLaunch(customHostLauncher, message); } + else if (string.Equals(MessageType.EditorAttachDebugger, message.MessageType)) + { + AttachDebuggerToProcess(customHostLauncher, message); + } } } catch (Exception exception) @@ -770,5 +778,33 @@ private void HandleCustomHostLaunch(ITestHostLauncher customHostLauncher, Messag this.communicationManager.SendMessage(MessageType.CustomTestHostLaunchCallback, ackPayload, this.protocolVersion); } } + + private void AttachDebuggerToProcess(ITestHostLauncher customHostLauncher, Message message) + { + var ackPayload = new EditorAttachDebuggerAckPayload() { Attached = false, ErrorMessage = null }; + + try + { + var pid = this.dataSerializer.DeserializePayload(message); + + ackPayload.Attached = customHostLauncher is ITestHostLauncher2 launcher + ? launcher.AttachDebuggerToProcess(pid) + : false; + } + catch (Exception ex) + { + EqtTrace.Error("VsTestConsoleRequestSender.AttachDebuggerToProcess: Error while attaching debugger to process: {0}", ex); + + // vstest.console will send the abort message properly while cleaning up all the + // flow, so do not abort here. + // Let the ack go through and let vstest.console handle the error. + ackPayload.ErrorMessage = ex.Message; + } + finally + { + // Always unblock the vstest.console thread which is indefintitely waiting on this ACK. + this.communicationManager.SendMessage(MessageType.EditorAttachDebuggerCallback, ackPayload, this.protocolVersion); + } + } } } diff --git a/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/CustomTestHostLauncher.cs b/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/CustomTestHostLauncher.cs index f96f9d7efa..7712b11c7c 100644 --- a/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/CustomTestHostLauncher.cs +++ b/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/CustomTestHostLauncher.cs @@ -11,7 +11,7 @@ namespace Microsoft.TestPlatform.AcceptanceTests.TranslationLayerTests /// /// The custom test host launcher. /// - public class CustomTestHostLauncher : ITestHostLauncher + public class CustomTestHostLauncher : ITestHostLauncher2 { public int ProcessId { @@ -22,6 +22,10 @@ public int ProcessId /// public bool IsDebug => true; + public bool AttachDebuggerToProcess(int pid) => this.AttachDebuggerToProcess(pid, CancellationToken.None); + + public bool AttachDebuggerToProcess(int pid, CancellationToken cancellationToken) => true; + /// public int LaunchTestHost(TestProcessStartInfo defaultTestHostStartInfo) { diff --git a/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/EventHandler/RunEventHandler.cs b/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/EventHandler/RunEventHandler.cs index b2aebf9da1..f3e1793d30 100644 --- a/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/EventHandler/RunEventHandler.cs +++ b/test/Microsoft.TestPlatform.AcceptanceTests/TranslationLayerTests/EventHandler/RunEventHandler.cs @@ -11,7 +11,7 @@ namespace Microsoft.TestPlatform.AcceptanceTests.TranslationLayerTests using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; /// - public class RunEventHandler : ITestRunEventsHandler + public class RunEventHandler : ITestRunEventsHandler2 { /// /// Gets the test results. @@ -90,5 +90,11 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta // No op return -1; } + + public bool AttachDebuggerToProcess(int pid) + { + // No op + return true; + } } } diff --git a/test/Microsoft.TestPlatform.Client.UnitTests/DesignMode/DesignModeClientTests.cs b/test/Microsoft.TestPlatform.Client.UnitTests/DesignMode/DesignModeClientTests.cs index ba94fb41e4..98befe71f8 100644 --- a/test/Microsoft.TestPlatform.Client.UnitTests/DesignMode/DesignModeClientTests.cs +++ b/test/Microsoft.TestPlatform.Client.UnitTests/DesignMode/DesignModeClientTests.cs @@ -38,7 +38,7 @@ public class DesignModeClientTests private readonly DesignModeClient designModeClient; - private readonly int protocolVersion = 1; + private readonly int protocolVersion = 3; private readonly AutoResetEvent complateEvent; @@ -480,7 +480,7 @@ public void InvokeCustomHostLaunchAckCallback(int processId, string errorMessage HostProcessId = processId, ErrorMessage = errorMessage }; - this.onAckMessageReceived?.Invoke( + this.onCustomTestHostLaunchAckReceived?.Invoke( new Message() { MessageType = MessageType.CustomTestHostLaunchCallback, Payload = JToken.FromObject(payload) }); } } diff --git a/test/Microsoft.TestPlatform.Client.UnitTests/DesignMode/DesignModeTestHostLauncherFactoryTests.cs b/test/Microsoft.TestPlatform.Client.UnitTests/DesignMode/DesignModeTestHostLauncherFactoryTests.cs index e2098781cf..79d961f685 100644 --- a/test/Microsoft.TestPlatform.Client.UnitTests/DesignMode/DesignModeTestHostLauncherFactoryTests.cs +++ b/test/Microsoft.TestPlatform.Client.UnitTests/DesignMode/DesignModeTestHostLauncherFactoryTests.cs @@ -29,7 +29,7 @@ public void DesignModeTestHostFactoryShouldReturnDebugLauncherIfDebuggingEnabled var testRunRequestPayload = new TestRunRequestPayload { DebuggingEnabled = true }; var launcher = DesignModeTestHostLauncherFactory.GetCustomHostLauncherForTestRun(mockDesignModeClient.Object, testRunRequestPayload); - Assert.IsTrue(launcher.IsDebug, "Factory must not return debug launcher if debugging is disabled."); + Assert.IsTrue(launcher.IsDebug, "Factory must return non-debug launcher if debugging is enabled."); } } } diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/EventHandlers/TestRequestHandlerTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/EventHandlers/TestRequestHandlerTests.cs index d90d5e4b2b..a3af85cdf4 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/EventHandlers/TestRequestHandlerTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/EventHandlers/TestRequestHandlerTests.cs @@ -514,14 +514,29 @@ private void VerifyResponseMessageContains(string message) public class TestableTestRequestHandler : TestRequestHandler { - public TestableTestRequestHandler(TestHostConnectionInfo testHostConnectionInfo,ICommunicationEndpointFactory communicationEndpointFactory, IDataSerializer dataSerializer, JobQueue jobQueue) - : base(testHostConnectionInfo, communicationEndpointFactory, dataSerializer, jobQueue, OnAckMessageReceived) + public TestableTestRequestHandler( + TestHostConnectionInfo testHostConnectionInfo, + ICommunicationEndpointFactory communicationEndpointFactory, + IDataSerializer dataSerializer, + JobQueue jobQueue) + : base( + testHostConnectionInfo, + communicationEndpointFactory, + dataSerializer, + jobQueue, + OnLaunchAdapterProcessWithDebuggerAttachedAckReceived, + OnAttachDebuggerAckRecieved) { } - private static void OnAckMessageReceived(Message message) + private static void OnLaunchAdapterProcessWithDebuggerAttachedAckReceived(Message message) { Assert.AreEqual(message.MessageType, MessageType.LaunchAdapterProcessWithDebuggerAttachedCallback); } + + private static void OnAttachDebuggerAckRecieved(Message message) + { + Assert.AreEqual(message.MessageType, MessageType.AttachDebuggerCallback); + } } } diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Execution/BaseRunTestsTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Execution/BaseRunTestsTests.cs index a8d87bda33..45bce131bd 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Execution/BaseRunTestsTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Execution/BaseRunTestsTests.cs @@ -1045,6 +1045,14 @@ protected override void SendSessionStart() { this.testCaseEventsHandler?.SendSessionStart(new Dictionary { { "TestSources", new List() { "1.dll" } } }); } + + protected override bool ShouldAttachDebuggerToTestHost( + LazyExtension executor, + Tuple executorUri, + RunContext runContext) + { + return false; + } } [ExtensionUri(BaseRunTestsExecutorUri)] diff --git a/test/Microsoft.TestPlatform.PerformanceTests/TranslationLayer/EventHandler/RunEventHandler.cs b/test/Microsoft.TestPlatform.PerformanceTests/TranslationLayer/EventHandler/RunEventHandler.cs index 0cb599140a..da3ec378d9 100644 --- a/test/Microsoft.TestPlatform.PerformanceTests/TranslationLayer/EventHandler/RunEventHandler.cs +++ b/test/Microsoft.TestPlatform.PerformanceTests/TranslationLayer/EventHandler/RunEventHandler.cs @@ -10,7 +10,7 @@ namespace Microsoft.TestPlatform.PerformanceTests.TranslationLayer using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; /// - public class RunEventHandler : ITestRunEventsHandler + public class RunEventHandler : ITestRunEventsHandler2 { /// /// Gets the test results. @@ -75,5 +75,11 @@ public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessSta // No op return -1; } + + public bool AttachDebuggerToProcess(int pid) + { + // No op + return true; + } } } diff --git a/test/TranslationLayer.UnitTests/VsTestConsoleRequestSenderTests.cs b/test/TranslationLayer.UnitTests/VsTestConsoleRequestSenderTests.cs index 5bbca72fa4..457856254d 100644 --- a/test/TranslationLayer.UnitTests/VsTestConsoleRequestSenderTests.cs +++ b/test/TranslationLayer.UnitTests/VsTestConsoleRequestSenderTests.cs @@ -39,7 +39,7 @@ public class VsTestConsoleRequestSenderTests private readonly int WaitTimeout = 2000; - private int protocolVersion = 2; + private int protocolVersion = 3; private IDataSerializer serializer = JsonDataSerializer.Instance; public VsTestConsoleRequestSenderTests()