Introduce APIs to improve debugging support in Visual Studio for tests that run in an external process distinct from testhost*.exe
.
Some test frameworks (examples include TAEF and Python) need to execute tests in a external process distinct from testhost*.exe
. When it comes to debugging such tests in Visual Studio today, there are a couple of problems.
-
A test adapter can request to launch a child process with debugger attached by calling
IFrameworkHandle.LaunchProcessWithDebuggerAttached()
within adapter's implementation ofITestExecutor.RunTests()
(after checking thatIRunContext.IsBeingDebugged
istrue
). However, there is no supported way for a test adapter to request that debugger should be attached to an already running process. -
Even though a test adapter can launch a child process with debugger attached as described above, today the debugger is always attached to the
testhost*.exe
process as well. This means that Visual Studio ends up debugging two processes instead of just the single process where tests are running.
Debugging of Python tests is supported in VS today. However, the Python adapter works around the above limiations by talking to the Python extension inside VS whenever ITestExecutor.RunTests()
is invoked (and IRunContext.IsBeingDebugged
is true
). The Python extension in VS then attaches VS debugger to the Python test process independently and also detaches the testhost*.exe
process to achieve the desired behavior.
While this works for Python, not all test frameworks that need to support debugging of tests running in external processes would want their users to also install a VS extension. For this reason, debugging of TAEF tests is currently not supported in VS.
- Introduce a new interface
IFrameworkHandle2
that inheritsIFrameworkHandle
and adds the followingAttachDebuggerToProcess()
API that an adapter can invoke from withinITestExecutor.RunTests()
.
namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter
{
/// <summary>
/// Handle to the framework which is passed to the test executors.
/// </summary>
public interface IFrameworkHandle2 : IFrameworkHandle
{
/// <summary>
/// Attach debugger to an already running process.
/// </summary>
/// <param name="pid">Process ID of the process to which the debugger should be attached.</param>
/// <returns><see cref="true"/> if the debugger was successfully attached to the requested process, <see cref="false"/> otherwise.</returns>
bool AttachDebuggerToProcess(int pid);
}
}
// Adapter's implementation of ITestExecutor.RunTests()
void ITestExecutor.RunTests(IEnumerable<TestCase> tests, IRunContext runContext, IFrameworkHandle frameworkHandle)
{
...
if (runContext.IsBeingDebugged && frameworkHandle is IFrameworkHandle2 frameworkHandle2)
{
frameworkHandle2.AttachDebuggerToProcess(testProcessId);
}
...
}
- [Optional] Introduce a new
ITestExecutor2
interface that inheritsITestExecutor
and adds the followingRunTests()
API. Newer adapters can choose to implementITestExecutor2
instead ofITestExecutor
. This would allow them to accessIFrameworkHandle2
without needing to cast fromIFrameworkHandle
toIFrameworkHandle2
.
namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter
{
/// <summary>
/// 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.
/// </summary>
public interface ITestExecutor2 : ITestExecutor
{
/// <summary>
/// Runs only the tests specified by parameter 'tests'.
/// </summary>
/// <param name="tests">Tests to be run.</param>
/// <param name="runContext">Context to use when executing the tests.</param>
/// <param param name="frameworkHandle">Handle to the framework to record results and to do framework operations.</param>
void RunTests(IEnumerable<TestCase> tests, IRunContext runContext, IFrameworkHandle2 frameworkHandle);
/// <summary>
/// Runs 'all' the tests present in the specified 'sources'.
/// </summary>
/// <param name="sources">Path to test container files to look for tests in.</param>
/// <param name="runContext">Context to use when executing the tests.</param>
/// <param param name="frameworkHandle">Handle to the framework to record results and to do framework operations.</param>
void RunTests(IEnumerable<string> sources, IRunContext runContext, IFrameworkHandle2 frameworkHandle);
}
}
- Introduce a new
ITestHostLauncher2
interface that inheritsITestHostLauncher
and adds the followingAttachDebuggerToProcess()
API. Visual Studio's Test Explorer will supply an implementation of this interface viaIVsTestConsoleWrapper.RunTestsWithCustomTestHost()
andITestHostLauncher2.AttachDebuggerToProcess()
will be called whenIFrameworkHandler2.AttachDebuggerToProcess()
is called within an adapter.
namespace Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces
{
/// <summary>
/// Interface defining contract for custom test host implementations
/// </summary>
public interface ITestHostLauncher2 : ITestHostLauncher
{
/// <summary>
/// Attach debugger to already running custom test host process.
/// </summary>
/// <param name="pid">Process ID of the process to which the debugger should be attached.</param>
/// <returns><see cref="true"/> if the debugger was successfully attached to the requested process, <see cref="false"/> otherwise.</returns>
bool AttachDebuggerToProcess(int pid);
}
}
- [Optional] Introduce a new
IVsTestConsoleWrapper2
interface that inheritsIVsTestConsoleWrapper
and adds the followingRunTestsWithCustomTestHost()
APIs. Visual Studio's Test Explorer can call this API and directly supply instance ofITestHostLauncher2
without the needing to cast fromITestHostLauncher2
toITestHostLauncher
.
namespace Microsoft.TestPlatform.VsTestConsole.TranslationLayer.Interfaces
{
/// <summary>
/// Controller for various test operations on the test runner.
/// </summary>
public interface IVsTestConsoleWrapper2 : IVsTestConsoleWrapper
{
/// <summary>
/// Starts a test run given a list of sources by giving caller an option to start their own test host.
/// </summary>
/// <param name="sources">Sources to Run tests on</param>
/// <param name="runSettings">RunSettings XML to run the tests</param>
/// <param name="testRunEventsHandler">EventHandler to receive test run events</param>
/// <param name="customTestHostLauncher">Custom test host launcher for the run.</param>
void RunTestsWithCustomTestHost(IEnumerable<string> sources, string runSettings, ITestRunEventsHandler testRunEventsHandler, ITestHostLauncher2 customTestHostLauncher);
/// <summary>
/// Starts a test run given a list of sources by giving caller an option to start their own test host.
/// </summary>
/// <param name="sources">Sources to Run tests on</param>
/// <param name="runSettings">RunSettings XML to run the tests</param>
/// <param name="options">Options to be passed into the platform.</param>
/// <param name="testRunEventsHandler">EventHandler to receive test run events</param>
/// <param name="customTestHostLauncher">Custom test host launcher for the run.</param>
void RunTestsWithCustomTestHost(IEnumerable<string> sources, string runSettings, TestPlatformOptions options, ITestRunEventsHandler testRunEventsHandler, ITestHostLauncher2 customTestHostLauncher);
/// <summary>
/// Starts a test run given a list of test cases by giving caller an option to start their own test host
/// </summary>
/// <param name="testCases">TestCases to run.</param>
/// <param name="runSettings">RunSettings XML to run the tests.</param>
/// <param name="testRunEventsHandler">EventHandler to receive test run events.</param>
/// <param name="customTestHostLauncher">Custom test host launcher for the run.</param>
void RunTestsWithCustomTestHost(IEnumerable<TestCase> testCases, string runSettings, ITestRunEventsHandler testRunEventsHandler, ITestHostLauncher2 customTestHostLauncher);
/// <summary>
/// Starts a test run given a list of test cases by giving caller an option to start their own test host
/// </summary>
/// <param name="testCases">TestCases to run.</param>
/// <param name="runSettings">RunSettings XML to run the tests.</param>
/// <param name="options">Options to be passed into the platform.</param>
/// <param name="testRunEventsHandler">EventHandler to receive test run events.</param>
/// <param name="customTestHostLauncher">Custom test host launcher for the run.</param>
void RunTestsWithCustomTestHost(IEnumerable<TestCase> testCases, string runSettings, TestPlatformOptions options, ITestRunEventsHandler testRunEventsHandler, ITestHostLauncher2 customTestHostLauncher);
}
}
-
Introduce a new well-known
TestProperty
-TestCaseProperties.UsesCustomTestHostProcess
- that an adapter can tack on toTestCase
s that will execute in a separate external process distinct fromtesthost*.exe
. If all selected tests in a debugging session have this property attached, then the Test Platform and Visual Studio can avoid attaching debugger to thetesthost*.exe
process.This may have some perf implication when debugging large selections (as the check will be performed even in cases where the property is not present). However, the scenario where someone wants to debug a large enough number of
TestCase
s where this would become a problem should be rare. I assume that the overhead of checking presence of oneTestProperty
on the selectedTestCases
s should not be huge for most regular debugging operations.
namespace Microsoft.VisualStudio.TestPlatform.ObjectModel
{
...
public static class TestCaseProperties
{
...
public static readonly TestProperty UsesCustomTestHostProcess =
TestProperty.Register(
id: "TestCase.UsesCustomTestHostProcess",
label: "UsesCustomTestHostProcess",
category: string.Empty,
description: string.Empty,
valueType: typeof(bool),
validateValueCallback: (object value) => value is bool,
attributes: TestPropertyAttributes.Hidden,
owner: typeof(TestCase));
...
}
...
}