12 KiB
Implementation Details
"How This Whole Crazy Thing Works: An extended apology"
by Steve Dower
Intent
We are building an extension for Visual Studio that integrates into many (most) of its features. We would like to automatically test this extension.
In general, automated tests for extensions are most efficient when the platform is simulated ("mocked out"). Over time, we aim to mock out as much of VS as we can, but integration testing can only reasonably take place within a running VS instance.
This package (VSTestHost) allows automated tests to be launched from VS or MSTest that run in the context of a Visual Studio instance. This enables developers to validate the product in its running context.
Making all of this work is messy, and this document is meant to help whoever has to touch it next.
Requirements
- Write and run tests using the normal VS unit testing framework
- Execute tests in a selectable VS instance (SKU, version, hive, etc.)
- Debug tests by selecting "Debug selected tests" within VS
- Resilient to VS crashes during test runs
Architecture
There are three executable processes involved when running these tests. Within VSTestHost, they are referred to as the TESTER, the TESTEE, and the EXECUTION ENGINE. Another concept is the TEST ADAPTER, which is a .NET class.
The TESTER is the instance of Visual Studio that the developer is using to launch tests. If the developer is debugging tests, the TESTER is the VS instance that will attach to the other instance.
When running tests from the command line, there is no TESTER.
The TESTEE is the instance of Visual Studio where the test will actually run. The testsettings file selected by the TESTER will determine which version, SKU, and hive of Visual Studio will be started as the TESTEE.
The TESTEE is launched and terminated by the TEST ADAPTER from the EXECUTION ENGINE
The EXECUTION ENGINE is a separate process that allows unit tests to run with different process-wide settings to what the TESTER was launched with. When tests are launched from the TESTER, it will start the EXECUTION ENGINE with a request to start running the selected tests.
- The EXECUTION ENGINE is not aware of the TESTER (which may not exist)
The TEST ADAPTER is a .NET class that is loaded in the EXECUTION ENGINE and controls the test execution sequence. The TEST ADAPTER is initialized for a run and is notified when the run is paused, resumed, stopped, or aborted. The TEST ADAPTER is passed each test in turn to be executed and the result passed back via a result sink.
VSTestHost includes two TEST ADAPTERs to support the IPC required to run tests in the TESTEE. The TEST ADAPTER loaded in the EXECUTION ENGINE is responsible for marshalling calls (including error handling) to the TESTEE's TEST ADAPTER, which is responsible for executing the test.
Neither TEST ADAPTER is the "real" unit test adapter, so executing a test looks more like instantiating another ITestAdapter and invoking its Run().
Test Settings
VSTestHost depends on the use of a testsettings file or TestProperty attributes to specify the SKU and hive of VS to use as the TESTEE. Setting the version is possible but not recommended - by default, the same version will be used for the TESTEE as the TESTER, which is most stable. The available settings are as follows:
Setting | Description |
---|---|
VSApplication | The registry key name, like "VisualStudio" or "WDExpress" |
VSExecutable | The executable name, like "devenv" or "wdexpress" |
VSVersion | The version number, like "12.0" or "14.0" |
VSHive | The hive name, like "Exp" or "Default" |
VSLaunchTimeoutInSeconds [opt] | The number of seconds to wait for launch |
VSDebugMixedMode | True to use mixed-mode debugging for tests |
ScreenCapture [opt] | Relative path to capture screenshots to |
ScreenCaptureInterval [opt] | Milliseconds between screenshots |
Note that identical screenshots are not saved.
Run Test Sequence
Because a TESTEE instance has no way of knowing whether it is actually a TESTEE or just an instance of VS with VSTestHost installed, all instances will open an IPC server and wait a short period of time for incoming connections. This server channel is uniquely named for the process and will not collide with other VS instances. The TESTEE's TEST ADAPTER is made available over this IPC channel.
The typical execution sequence looks like this:
- Developer clicks "Run selected test" in the TESTER
- TESTER launches EXECUTION ENGINE
- EXECUTION ENGINE loads the TEST ADAPTER
- EXECUTION ENGINE calls Initialize() on the TEST ADAPTER
- EXECUTION ENGINE's TEST ADAPTER reads the configuration and launches the TESTEE
- TESTEE opens an IPC channel that is unique to the process and publishes its own TEST ADAPTER
- EXECUTION ENGINE's TEST ADAPTER connects to the TESTEE's TEST ADAPTER and returns from Initialize().
- EXECUTION ENGINE calls its TEST ADAPTER's Run() method for each test.
- EXECUTION ENGINE's TEST ADAPTER calls TESTEE's TEST ADAPTER's Run() method
- TESTEE's TEST ADAPTER instantiates/caches the real unit test adapter
- TESTEE's TEST ADAPTER invokes the unit test adapter's Run() method
- Repeat 8-11 for each test.
- EXECUTION ENGINE calls its TEST ADAPTER's PreTestRunFinished() and Cleanup() methods
- EXECUTION ENGINE's TEST ADAPTER exits the TESTEE.
- Test run is complete.
Debug Test Sequence
Debugging is more complicated, because the TESTER needs to perform debug/attach but only the EXECUTION ENGINE knows the TESTEE's process ID. However, the EXECUTION ENGINE does not know the TESTER's process ID, and so cannot send a message directly to the TESTER. Instead, we use a global IPC server that is only active for a period of time after the TESTER begins debugging.
Reliably detecting when a debugging session is for a unit test requires extra dependencies for the package loaded in the TESTER, which may prevent the assembly from loading or being useful in as many VS configurations as we would like. With our current architecture, we have a single DLL that is installed into the GAC and is loaded in all instances of VS, whether they will be the TESTER, the TESTEE, or are not taking part in unit tests at all. An alternate implementation would require three assemblies with two VSPackages and at least one vsixmanifest for the TESTER's package, as well as multiple dependencies between these assemblies and other dependencies that lead to a complicated and error-prone deployment process.
Rather than dealing with this, we will open the debug IPC channel each time any VS instance starts debugging. If the channel is already open, the new instance will connect to it and signal it to terminate, so that the most recent debugging session is the one listening. This results in a potential race if the user starts multiple debugging sessions where at least one is supposed to attach to a TESTEE, but since this race is with a human, it is considered unlikely. If the EXECUTION ENGINE needs to attach but is unable to find an IPC server, it will abort the run and no tests are executed. The workaround is to restart debugging. There will also be more IPC channels opened than necessary, since we will open one for every debugging session unless we know the instance is a TESTEE. These are not much more expensive than creating a named pipe, so it is considered a worthwhile cost.
In short, debugging works fine unless you try to break it (for example, by starting to debug a test then quickly starting a separate debugging session in another VS instance).
The typical debug execution sequence looks like this: (Steps prefixed with an asterisk are different from the Run Test sequence.)
- *Developer clicks "Debug selected test" in the TESTER
- *TESTER launches EXECUTION ENGINE under its managed debugger.
- *TESTER opens the debug IPC server. If another process has opened the server already, then TESTER tells it to close.
- EXECUTION ENGINE loads the TEST ADAPTER
- EXECUTION ENGINE calls Initialize() on the TEST ADAPTER
- EXECUTION ENGINE's TEST ADAPTER reads the configuration and launches the TESTEE
- TESTEE opens an IPC channel that is unique to the process and publishes its own TEST ADAPTER
- *EXECUTION ENGINE's TEST ADAPTER connects to the TESTEE's TEST ADAPTER
- *EXECUTION ENGINE's TEST ADAPTER connects to the debug IPC server and tells it to attach to TESTEE.
- *EXECUTION ENGINE's TEST ADAPTER returns from Initialize().
- *TESTER attaches its debugger to TESTEE.
- EXECUTION ENGINE calls its TEST ADAPTER's Run() method for each test.
- EXECUTION ENGINE's TEST ADAPTER calls TESTEE's TEST ADAPTER's Run() method
- TESTEE's TEST ADAPTER instantiates/caches the real unit test adapter
- TESTEE's TEST ADAPTER invokes the unit test adapter's Run() method
- Repeat 12-15 for each test.
- EXECUTION ENGINE calls its TEST ADAPTER's PreTestRunFinished() and Cleanup() methods
- EXECUTION ENGINE's TEST ADAPTER exits the TESTEE.
- Test run is complete.
VSTestHost Classes
This is an overview of the classes within VSTestHost and how they are used. Some classes are used in multiple places with slightly different purposes, and because not every VS SKU or version can support being a TESTER, some parts are #if'd out to allow separate builds for those. Because of the reference to Microsoft.VisualStudio.Shell.##.0, each VS version requires its own build of VSTestHost.
VSTestHostPackage
This package is auto-loaded in every instance of VS.
For TESTEEs (bearing in mind that we always initially assume this), package initialization creates the IPC channel and publishes its test adapter. This channel is not closed until the package is disposed at process exit.
For TESTERs (again, this is always assumed initially), the debugging state is hooked so the debug IPC channel can be activated/deactivated as necessary. Channel creation is handled by the static members of TesterDebugAttacher. When the timeout expires, another TESTER manually aborts the wait, or an EXECUTION ENGINE signals to attach, the channel is closed.
TesterDebugAttacher
This class is published over the debug IPC channel to allow the EXECUTION ENGINE to instruct the debugger in the TESTER to attach to the TESTEE. Other TESTERs may also use it to abort listeners so they can take over the channel.
The class has static methods for use by the TESTER to simplify listening for commands.
TesterTestAdapter
This class is loaded in the EXECUTION ENGINE to run tests that have a [HostType("VSTestHost")] attribute. It is largely responsible for connecting to an instance of TesteeTestAdapter and marshalling calls to the TESTEE.
TesteeTestAdapter
This class is published from the TESTEE on the unique IPC channel created by VSTestHostPackage. It implements the logic for running arbitrary tests within VS by loading the intended test adapter.
VisualStudio
This class encapsulates the logic for starting VS and getting its DTE object from the EXECUTION ENGINE. Currently, DTE is only used by the EXECUTION ENGINE to safely exit VS at the end of a test run, but in the future could be used for tests that need to drive VS from outside the TESTEE process.
VSTestContext
This class is used by tests running within the TESTEE to easily access the VS instance's DTE and global ServiceProvider objects. Unit test projects should include the following reference for VSTestHost:
<Reference Include="Microsoft.VisualStudioTools.VSTestHost.$(VSTarget)" />
Test projects that do not refer to VSTestContext
do not require this reference.
VSTestContext
is the only public and supported class in the
Microsoft.VisualStudioTools.VSTestHost assembly. All other public classes are
infrastructure.