diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..176a458
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/.gitignore b/.gitignore
index 3e759b7..19dafde 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,330 +1,8 @@
-## Ignore Visual Studio temporary files, build results, and
-## files generated by popular Visual Studio add-ons.
-##
-## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+# Files created by the build
+bin/
+obj/
-# User-specific files
-*.suo
-*.user
-*.userosscache
-*.sln.docstates
-
-# User-specific files (MonoDevelop/Xamarin Studio)
-*.userprefs
-
-# Build results
-[Dd]ebug/
-[Dd]ebugPublic/
-[Rr]elease/
-[Rr]eleases/
-x64/
-x86/
-bld/
-[Bb]in/
-[Oo]bj/
-[Ll]og/
-
-# Visual Studio 2015/2017 cache/options directory
+# Per-user files
.vs/
-# Uncomment if you have tasks that create the project's static files in wwwroot
-#wwwroot/
-
-# Visual Studio 2017 auto generated files
-Generated\ Files/
-
-# MSTest test Results
-[Tt]est[Rr]esult*/
-[Bb]uild[Ll]og.*
-
-# NUNIT
-*.VisualState.xml
-TestResult.xml
-
-# Build Results of an ATL Project
-[Dd]ebugPS/
-[Rr]eleasePS/
-dlldata.c
-
-# Benchmark Results
-BenchmarkDotNet.Artifacts/
-
-# .NET Core
-project.lock.json
-project.fragment.lock.json
-artifacts/
-**/Properties/launchSettings.json
-
-# StyleCop
-StyleCopReport.xml
-
-# Files built by Visual Studio
-*_i.c
-*_p.c
-*_i.h
-*.ilk
-*.meta
-*.obj
-*.iobj
-*.pch
-*.pdb
-*.ipdb
-*.pgc
-*.pgd
-*.rsp
-*.sbr
-*.tlb
-*.tli
-*.tlh
-*.tmp
-*.tmp_proj
-*.log
-*.vspscc
-*.vssscc
-.builds
-*.pidb
-*.svclog
-*.scc
-
-# Chutzpah Test files
-_Chutzpah*
-
-# Visual C++ cache files
-ipch/
-*.aps
-*.ncb
-*.opendb
-*.opensdf
-*.sdf
-*.cachefile
-*.VC.db
-*.VC.VC.opendb
-
-# Visual Studio profiler
-*.psess
-*.vsp
-*.vspx
-*.sap
-
-# Visual Studio Trace Files
-*.e2e
-
-# TFS 2012 Local Workspace
-$tf/
-
-# Guidance Automation Toolkit
-*.gpState
-
-# ReSharper is a .NET coding add-in
-_ReSharper*/
-*.[Rr]e[Ss]harper
-*.DotSettings.user
-
-# JustCode is a .NET coding add-in
-.JustCode
-
-# TeamCity is a build add-in
-_TeamCity*
-
-# DotCover is a Code Coverage Tool
-*.dotCover
-
-# AxoCover is a Code Coverage Tool
-.axoCover/*
-!.axoCover/settings.json
-
-# Visual Studio code coverage results
-*.coverage
-*.coveragexml
-
-# NCrunch
-_NCrunch_*
-.*crunch*.local.xml
-nCrunchTemp_*
-
-# MightyMoose
-*.mm.*
-AutoTest.Net/
-
-# Web workbench (sass)
-.sass-cache/
-
-# Installshield output folder
-[Ee]xpress/
-
-# DocProject is a documentation generator add-in
-DocProject/buildhelp/
-DocProject/Help/*.HxT
-DocProject/Help/*.HxC
-DocProject/Help/*.hhc
-DocProject/Help/*.hhk
-DocProject/Help/*.hhp
-DocProject/Help/Html2
-DocProject/Help/html
-
-# Click-Once directory
-publish/
-
-# Publish Web Output
-*.[Pp]ublish.xml
-*.azurePubxml
-# Note: Comment the next line if you want to checkin your web deploy settings,
-# but database connection strings (with potential passwords) will be unencrypted
-*.pubxml
-*.publishproj
-
-# Microsoft Azure Web App publish settings. Comment the next line if you want to
-# checkin your Azure Web App publish settings, but sensitive information contained
-# in these scripts will be unencrypted
-PublishScripts/
-
-# NuGet Packages
-*.nupkg
-# The packages folder can be ignored because of Package Restore
-**/[Pp]ackages/*
-# except build/, which is used as an MSBuild target.
-!**/[Pp]ackages/build/
-# Uncomment if necessary however generally it will be regenerated when needed
-#!**/[Pp]ackages/repositories.config
-# NuGet v3's project.json files produces more ignorable files
-*.nuget.props
-*.nuget.targets
-
-# Microsoft Azure Build Output
-csx/
-*.build.csdef
-
-# Microsoft Azure Emulator
-ecf/
-rcf/
-
-# Windows Store app package directories and files
-AppPackages/
-BundleArtifacts/
-Package.StoreAssociation.xml
-_pkginfo.txt
-*.appx
-
-# Visual Studio cache files
-# files ending in .cache can be ignored
-*.[Cc]ache
-# but keep track of directories ending in .cache
-!*.[Cc]ache/
-
-# Others
-ClientBin/
-~$*
-*~
-*.dbmdl
-*.dbproj.schemaview
-*.jfm
-*.pfx
-*.publishsettings
-orleans.codegen.cs
-
-# Including strong name files can present a security risk
-# (https://github.com/github/gitignore/pull/2483#issue-259490424)
-#*.snk
-
-# Since there are multiple workflows, uncomment next line to ignore bower_components
-# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
-#bower_components/
-
-# RIA/Silverlight projects
-Generated_Code/
-
-# Backup & report files from converting an old project file
-# to a newer Visual Studio version. Backup files are not needed,
-# because we have git ;-)
-_UpgradeReport_Files/
-Backup*/
-UpgradeLog*.XML
-UpgradeLog*.htm
-ServiceFabricBackup/
-*.rptproj.bak
-
-# SQL Server files
-*.mdf
-*.ldf
-*.ndf
-
-# Business Intelligence projects
-*.rdl.data
-*.bim.layout
-*.bim_*.settings
-*.rptproj.rsuser
-
-# Microsoft Fakes
-FakesAssemblies/
-
-# GhostDoc plugin setting file
-*.GhostDoc.xml
-
-# Node.js Tools for Visual Studio
-.ntvs_analysis.dat
-node_modules/
-
-# Visual Studio 6 build log
-*.plg
-
-# Visual Studio 6 workspace options file
-*.opt
-
-# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
-*.vbw
-
-# Visual Studio LightSwitch build output
-**/*.HTMLClient/GeneratedArtifacts
-**/*.DesktopClient/GeneratedArtifacts
-**/*.DesktopClient/ModelManifest.xml
-**/*.Server/GeneratedArtifacts
-**/*.Server/ModelManifest.xml
-_Pvt_Extensions
-
-# Paket dependency manager
-.paket/paket.exe
-paket-files/
-
-# FAKE - F# Make
-.fake/
-
-# JetBrains Rider
-.idea/
-*.sln.iml
-
-# CodeRush
-.cr/
-
-# Python Tools for Visual Studio (PTVS)
-__pycache__/
-*.pyc
-
-# Cake - Uncomment if you are using it
-# tools/**
-# !tools/packages.config
-
-# Tabs Studio
-*.tss
-
-# Telerik's JustMock configuration file
-*.jmconfig
-
-# BizTalk build output
-*.btp.cs
-*.btm.cs
-*.odx.cs
-*.xsd.cs
-
-# OpenCover UI analysis results
-OpenCover/
-
-# Azure Stream Analytics local run output
-ASALocalRun/
-
-# MSBuild Binary and Structured Log
-*.binlog
-
-# NVidia Nsight GPU debugger configuration file
-*.nvuser
-
-# MFractors (Xamarin productivity tool) working folder
-.mfractor/
+*.csproj.user
+*.shproj.user
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..72f1506
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,14 @@
+
+# Contributing
+
+This project welcomes contributions and suggestions. Most contributions require you to agree to a
+Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
+the rights to use your contribution. For details, visit https://cla.microsoft.com.
+
+When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
+a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
+provided by the bot. You will only need to do this once across all repos using our CLA.
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
+contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
diff --git a/ExtensionTesting.sln b/ExtensionTesting.sln
new file mode 100644
index 0000000..4e82e40
--- /dev/null
+++ b/ExtensionTesting.sln
@@ -0,0 +1,72 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27906.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Extensibility.Testing.Xunit", "src\Microsoft.VisualStudio.Extensibility.Testing.Xunit\Microsoft.VisualStudio.Extensibility.Testing.Xunit.csproj", "{C4C38EAE-B6B1-4EE5-9F01-03B4D9249BBD}"
+EndProject
+Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.VisualStudio.VsixInstaller.Shared", "src\Microsoft.VisualStudio.VsixInstaller.Shared\Microsoft.VisualStudio.VsixInstaller.Shared.shproj", "{B3BED6CB-ABFE-4BB8-8AF7-901FF0C6F027}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.VsixInstaller.Interop", "src\Microsoft.VisualStudio.VsixInstaller.Interop\Microsoft.VisualStudio.VsixInstaller.Interop.csproj", "{7D8374CB-5DBE-49AE-A251-82132EAEE3AA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.VsixInstaller.11", "src\Microsoft.VisualStudio.VsixInstaller.11\Microsoft.VisualStudio.VsixInstaller.11.csproj", "{784CB5C8-6258-499D-8EFB-7A601A6F7E46}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.VsixInstaller.12", "src\Microsoft.VisualStudio.VsixInstaller.12\Microsoft.VisualStudio.VsixInstaller.12.csproj", "{75EDEB61-DB9D-4F12-B922-ECEF8081CEF1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.VsixInstaller.14", "src\Microsoft.VisualStudio.VsixInstaller.14\Microsoft.VisualStudio.VsixInstaller.14.csproj", "{DA8322BC-E135-41BB-838A-D87551092E1A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.VsixInstaller.15", "src\Microsoft.VisualStudio.VsixInstaller.15\Microsoft.VisualStudio.VsixInstaller.15.csproj", "{4232A7BA-A7B7-4C65-B7A1-17FB5CDED299}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.IntegrationTestService", "src\Microsoft.VisualStudio.IntegrationTestService\Microsoft.VisualStudio.IntegrationTestService.csproj", "{2A5E7027-593B-413F-B4A1-0659941127B1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests", "src\Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests\Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests.csproj", "{346081A2-A088-4486-94B3-B586BD6FD888}"
+EndProject
+Global
+ GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ src\Microsoft.VisualStudio.VsixInstaller.Shared\Microsoft.VisualStudio.VsixInstaller.Shared.projitems*{b3bed6cb-abfe-4bb8-8af7-901ff0c6f027}*SharedItemsImports = 13
+ EndGlobalSection
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C4C38EAE-B6B1-4EE5-9F01-03B4D9249BBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4C38EAE-B6B1-4EE5-9F01-03B4D9249BBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4C38EAE-B6B1-4EE5-9F01-03B4D9249BBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4C38EAE-B6B1-4EE5-9F01-03B4D9249BBD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7D8374CB-5DBE-49AE-A251-82132EAEE3AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7D8374CB-5DBE-49AE-A251-82132EAEE3AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D8374CB-5DBE-49AE-A251-82132EAEE3AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7D8374CB-5DBE-49AE-A251-82132EAEE3AA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {784CB5C8-6258-499D-8EFB-7A601A6F7E46}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {784CB5C8-6258-499D-8EFB-7A601A6F7E46}.Debug|Any CPU.Build.0 = Debug|x86
+ {784CB5C8-6258-499D-8EFB-7A601A6F7E46}.Release|Any CPU.ActiveCfg = Release|x86
+ {784CB5C8-6258-499D-8EFB-7A601A6F7E46}.Release|Any CPU.Build.0 = Release|x86
+ {75EDEB61-DB9D-4F12-B922-ECEF8081CEF1}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {75EDEB61-DB9D-4F12-B922-ECEF8081CEF1}.Debug|Any CPU.Build.0 = Debug|x86
+ {75EDEB61-DB9D-4F12-B922-ECEF8081CEF1}.Release|Any CPU.ActiveCfg = Release|x86
+ {75EDEB61-DB9D-4F12-B922-ECEF8081CEF1}.Release|Any CPU.Build.0 = Release|x86
+ {DA8322BC-E135-41BB-838A-D87551092E1A}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {DA8322BC-E135-41BB-838A-D87551092E1A}.Debug|Any CPU.Build.0 = Debug|x86
+ {DA8322BC-E135-41BB-838A-D87551092E1A}.Release|Any CPU.ActiveCfg = Release|x86
+ {DA8322BC-E135-41BB-838A-D87551092E1A}.Release|Any CPU.Build.0 = Release|x86
+ {4232A7BA-A7B7-4C65-B7A1-17FB5CDED299}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {4232A7BA-A7B7-4C65-B7A1-17FB5CDED299}.Debug|Any CPU.Build.0 = Debug|x86
+ {4232A7BA-A7B7-4C65-B7A1-17FB5CDED299}.Release|Any CPU.ActiveCfg = Release|x86
+ {4232A7BA-A7B7-4C65-B7A1-17FB5CDED299}.Release|Any CPU.Build.0 = Release|x86
+ {2A5E7027-593B-413F-B4A1-0659941127B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A5E7027-593B-413F-B4A1-0659941127B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A5E7027-593B-413F-B4A1-0659941127B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2A5E7027-593B-413F-B4A1-0659941127B1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {346081A2-A088-4486-94B3-B586BD6FD888}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {346081A2-A088-4486-94B3-B586BD6FD888}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {346081A2-A088-4486-94B3-B586BD6FD888}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {346081A2-A088-4486-94B3-B586BD6FD888}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {BEAADCC8-C5EB-46B6-8AA8-0242304BE240}
+ EndGlobalSection
+EndGlobal
diff --git a/LICENSE b/LICENSE
index 2107107..5cf7c8d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,21 @@
- MIT License
+MIT License
- Copyright (c) Microsoft Corporation. All rights reserved.
+Copyright (c) Microsoft Corporation. All rights reserved.
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
diff --git a/README.md b/README.md
index 72f1506..67fed23 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1 @@
-
-# Contributing
-
-This project welcomes contributions and suggestions. Most contributions require you to agree to a
-Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
-the rights to use your contribution. For details, visit https://cla.microsoft.com.
-
-When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
-a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
-provided by the bot. You will only need to do this once across all repos using our CLA.
-
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
-For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
-contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+# Visual Studio Extension Testing
diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT
new file mode 100644
index 0000000..164b8ed
--- /dev/null
+++ b/THIRD-PARTY-NOTICES.TXT
@@ -0,0 +1,9 @@
+Visual Studio Extension Testing uses third-party libraries or other resources that may be
+distributed under licenses different than the Visual Studio Extension Testing software.
+
+In the event that we accidentally failed to list a required notice, please
+bring it to our attention. Post an issue or email us:
+
+ opencode@microsoft.com
+
+The attached notices are provided for information only.
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..bd1c6ce
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,32 @@
+
+
+
+
+ Copyright (c) Microsoft. All rights reserved.
+
+
+
+ strict
+
+
+
+
+ false
+ embedded
+ true
+ true
+
+
+
+
+ true
+ $(MSBuildThisFileDirectory)ExtensionTesting.ruleset
+
+
+
+
+
+
+
+
+
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
new file mode 100644
index 0000000..341027f
--- /dev/null
+++ b/src/Directory.Build.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/ExtensionTesting.ruleset b/src/ExtensionTesting.ruleset
new file mode 100644
index 0000000..032cfe6
--- /dev/null
+++ b/src/ExtensionTesting.ruleset
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/AbstractIdeIntegrationTest.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/AbstractIdeIntegrationTest.cs
new file mode 100644
index 0000000..86c30d5
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/AbstractIdeIntegrationTest.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests
+{
+ using System;
+ using System.Threading.Tasks;
+ using global::Xunit;
+ using global::Xunit.Harness;
+ using Microsoft.VisualStudio.Shell.Interop;
+ using Microsoft.VisualStudio.Threading;
+
+ public abstract class AbstractIdeIntegrationTest : IAsyncLifetime, IDisposable
+ {
+ private JoinableTaskContext _joinableTaskContext;
+ private JoinableTaskCollection _joinableTaskCollection;
+ private JoinableTaskFactory _joinableTaskFactory;
+
+ protected AbstractIdeIntegrationTest()
+ {
+ if (GlobalServiceProvider.ServiceProvider.GetService(typeof(SVsTaskSchedulerService)) is IVsTaskSchedulerService2 taskSchedulerService)
+ {
+ JoinableTaskContext = (JoinableTaskContext)taskSchedulerService.GetAsyncTaskContext();
+ }
+ else
+ {
+ JoinableTaskContext = new JoinableTaskContext();
+ }
+ }
+
+ protected JoinableTaskContext JoinableTaskContext
+ {
+ get
+ {
+ return _joinableTaskContext ?? throw new InvalidOperationException();
+ }
+
+ private set
+ {
+ if (value == _joinableTaskContext)
+ {
+ return;
+ }
+
+ if (value is null)
+ {
+ _joinableTaskContext = null;
+ _joinableTaskCollection = null;
+ _joinableTaskFactory = null;
+ }
+ else
+ {
+ _joinableTaskContext = value;
+ _joinableTaskCollection = value.CreateCollection();
+ _joinableTaskFactory = value.CreateFactory(_joinableTaskCollection);
+ }
+ }
+ }
+
+ protected JoinableTaskFactory JoinableTaskFactory => _joinableTaskFactory ?? throw new InvalidOperationException();
+
+ protected IServiceProvider ServiceProvider => GlobalServiceProvider.ServiceProvider;
+
+ public virtual Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public virtual async Task DisposeAsync()
+ {
+ await _joinableTaskCollection.JoinTillEmptyAsync();
+ JoinableTaskContext = null;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/IdeFactTest.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/IdeFactTest.cs
new file mode 100644
index 0000000..92b10a5
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/IdeFactTest.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests
+{
+ using System.Diagnostics;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using System.Windows.Threading;
+ using global::Xunit;
+ using global::Xunit.Threading;
+ using Microsoft.VisualStudio.Shell.Interop;
+ using Microsoft.VisualStudio.Threading;
+ using _DTE = EnvDTE._DTE;
+ using DTE = EnvDTE.DTE;
+
+ public class IdeFactTest : AbstractIdeIntegrationTest
+ {
+ [IdeFact]
+ public void TestOpenAndCloseIDE()
+ {
+ Assert.Equal("devenv", Process.GetCurrentProcess().ProcessName);
+ var dte = (DTE)ServiceProvider.GetService(typeof(_DTE));
+ Assert.NotNull(dte);
+ }
+
+ [IdeFact]
+ public void TestRunsOnUIThread()
+ {
+ Assert.True(Application.Current.Dispatcher.CheckAccess());
+ }
+
+ [IdeFact]
+ public async Task TestRunsOnUIThreadAsync()
+ {
+ Assert.True(Application.Current.Dispatcher.CheckAccess());
+ await Task.Yield();
+ Assert.True(Application.Current.Dispatcher.CheckAccess());
+ }
+
+ [IdeFact]
+ public async Task TestYieldsToWorkAsync()
+ {
+ Assert.True(Application.Current.Dispatcher.CheckAccess());
+ await Task.Factory.StartNew(
+ () => { },
+ CancellationToken.None,
+ TaskCreationOptions.None,
+ new SynchronizationContextTaskScheduler(new DispatcherSynchronizationContext(Application.Current.Dispatcher)));
+ Assert.True(Application.Current.Dispatcher.CheckAccess());
+ }
+
+ [IdeFact]
+ public async Task TestJoinableTaskFactoryAsync()
+ {
+ Assert.NotNull(JoinableTaskContext);
+ Assert.NotNull(JoinableTaskFactory);
+ Assert.Equal(Thread.CurrentThread, JoinableTaskContext.MainThread);
+
+ await TaskScheduler.Default;
+
+ Assert.NotEqual(Thread.CurrentThread, JoinableTaskContext.MainThread);
+
+ await JoinableTaskFactory.SwitchToMainThreadAsync();
+
+ Assert.Equal(Thread.CurrentThread, JoinableTaskContext.MainThread);
+ }
+
+ [IdeFact(MaxVersion = VisualStudioVersion.VS2012)]
+ public void TestJoinableTaskFactoryProvidedByTest()
+ {
+ var taskSchedulerServiceObject = ServiceProvider.GetService(typeof(SVsTaskSchedulerService));
+ Assert.NotNull(taskSchedulerServiceObject);
+
+ var taskSchedulerService = taskSchedulerServiceObject as IVsTaskSchedulerService;
+ Assert.NotNull(taskSchedulerService);
+
+ var taskSchedulerService2 = taskSchedulerServiceObject as IVsTaskSchedulerService2;
+ Assert.Null(taskSchedulerService2);
+
+ Assert.NotNull(JoinableTaskContext);
+ }
+
+ [IdeFact(MinVersion = VisualStudioVersion.VS2013)]
+ public void TestJoinableTaskFactoryObtainedFromEnvironment()
+ {
+ var taskSchedulerServiceObject = ServiceProvider.GetService(typeof(SVsTaskSchedulerService));
+ Assert.NotNull(taskSchedulerServiceObject);
+
+ var taskSchedulerService = taskSchedulerServiceObject as IVsTaskSchedulerService2;
+ Assert.NotNull(taskSchedulerService);
+
+ Assert.Same(JoinableTaskContext, taskSchedulerService.GetAsyncTaskContext());
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests.csproj b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests.csproj
new file mode 100644
index 0000000..0ef0c50
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests.csproj
@@ -0,0 +1,37 @@
+
+
+
+
+ net46
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..ff348f2
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/Properties/AssemblyInfo.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+using Xunit;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: CLSCompliant(false)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+[assembly: TestFramework("Xunit.Harness.IdeTestFramework", "Microsoft.VisualStudio.Extensibility.Testing.Xunit")]
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/xunit.runner.json b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/xunit.runner.json
new file mode 100644
index 0000000..4778b41
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit.IntegrationTests/xunit.runner.json
@@ -0,0 +1,3 @@
+{
+ "appDomain": "denied"
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/GlobalServiceProvider.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/GlobalServiceProvider.cs
new file mode 100644
index 0000000..2557724
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/GlobalServiceProvider.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using System.Threading;
+ using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
+
+ public static class GlobalServiceProvider
+ {
+ private static IServiceProvider _serviceProvider;
+
+ public static IServiceProvider ServiceProvider
+ {
+ get
+ {
+ return _serviceProvider ?? (_serviceProvider = GetGlobalServiceProvider());
+ }
+ }
+
+ private static IServiceProvider GetGlobalServiceProvider()
+ {
+ var oleMessageFilterForCallingThread = GetOleMessageFilterForCallingThread();
+ var oleServiceProvider = (IOleServiceProvider)oleMessageFilterForCallingThread;
+ return new ServiceProvider(oleServiceProvider);
+ }
+
+ private static object GetOleMessageFilterForCallingThread()
+ {
+ if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA)
+ {
+ return null;
+ }
+
+ if (NativeMethods.CoRegisterMessageFilter(IntPtr.Zero, out var oldMessageFilter) < 0)
+ {
+ return null;
+ }
+
+ if (oldMessageFilter == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ NativeMethods.CoRegisterMessageFilter(oldMessageFilter, out _);
+
+ try
+ {
+ return Marshal.GetObjectForIUnknown(oldMessageFilter);
+ }
+ finally
+ {
+ Marshal.Release(oldMessageFilter);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestAssemblyRunner.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestAssemblyRunner.cs
new file mode 100644
index 0000000..8ea0a61
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestAssemblyRunner.cs
@@ -0,0 +1,355 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Windows.Threading;
+ using Xunit.Abstractions;
+ using Xunit.Sdk;
+ using Xunit.Threading;
+
+ internal class IdeTestAssemblyRunner : XunitTestAssemblyRunner
+ {
+ ///
+ /// A long timeout used to avoid hangs in tests, where a test failure manifests as an operation never occurring.
+ ///
+ private static readonly TimeSpan HangMitigatingTimeout = TimeSpan.FromMinutes(1);
+
+ public IdeTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
+ : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions)
+ {
+ }
+
+ protected override async Task RunTestCollectionAsync(IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource)
+ {
+ var result = new RunSummary();
+ var testAssemblyFinishedMessages = new List();
+ var completedTestCaseIds = new HashSet();
+ try
+ {
+ ExecutionMessageSink.OnMessage(new TestAssemblyStarting(testCases, TestAssembly, DateTime.Now, GetTestFrameworkEnvironment(), GetTestFrameworkDisplayName()));
+ ExecutionMessageSink.OnMessage(new TestCollectionStarting(testCases, testCollection));
+
+ foreach (var testCasesByTargetVersion in testCases.GroupBy(GetVisualStudioVersionForTestCase))
+ {
+ using (var visualStudioInstanceFactory = new VisualStudioInstanceFactory())
+ {
+ var summary = await RunTestCollectionForVersionAsync(visualStudioInstanceFactory, testCasesByTargetVersion.Key, completedTestCaseIds, messageBus, testCollection, testCasesByTargetVersion, cancellationTokenSource);
+ result.Aggregate(summary.Item1);
+ testAssemblyFinishedMessages.Add(summary.Item2);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ var completedTestCases = testCases.Where(testCase => completedTestCaseIds.Contains(testCase.UniqueID));
+ var remainingTestCases = testCases.Except(completedTestCases);
+ foreach (var casesByTestClass in remainingTestCases.GroupBy(testCase => testCase.TestMethod.TestClass))
+ {
+ ExecutionMessageSink.OnMessage(new TestClassStarting(casesByTestClass.ToArray(), casesByTestClass.Key));
+
+ foreach (var casesByTestMethod in casesByTestClass.GroupBy(testCase => testCase.TestMethod))
+ {
+ ExecutionMessageSink.OnMessage(new TestMethodStarting(casesByTestMethod.ToArray(), casesByTestMethod.Key));
+
+ foreach (var testCase in casesByTestMethod)
+ {
+ ExecutionMessageSink.OnMessage(new TestCaseStarting(testCase));
+
+ var test = new XunitTest(testCase, testCase.DisplayName);
+ ExecutionMessageSink.OnMessage(new TestStarting(test));
+ ExecutionMessageSink.OnMessage(new TestFailed(test, 0, null, new InvalidOperationException("Test did not run due to a harness failure.", ex)));
+ ExecutionMessageSink.OnMessage(new TestFinished(test, 0, null));
+
+ ExecutionMessageSink.OnMessage(new TestCaseFinished(testCase, 0, 1, 1, 0));
+ }
+
+ ExecutionMessageSink.OnMessage(new TestMethodFinished(casesByTestMethod.ToArray(), casesByTestMethod.Key, 0, casesByTestMethod.Count(), casesByTestMethod.Count(), 0));
+ }
+
+ ExecutionMessageSink.OnMessage(new TestClassFinished(casesByTestClass.ToArray(), casesByTestClass.Key, 0, casesByTestClass.Count(), casesByTestClass.Count(), 0));
+ }
+
+ throw;
+ }
+ finally
+ {
+ var totalExecutionTime = testAssemblyFinishedMessages.Sum(message => message.ExecutionTime);
+ var testsRun = testAssemblyFinishedMessages.Sum(message => message.TestsRun);
+ var testsFailed = testAssemblyFinishedMessages.Sum(message => message.TestsFailed);
+ var testsSkipped = testAssemblyFinishedMessages.Sum(message => message.TestsSkipped);
+ ExecutionMessageSink.OnMessage(new TestCollectionFinished(testCases, testCollection, totalExecutionTime, testsRun, testsFailed, testsSkipped));
+ ExecutionMessageSink.OnMessage(new TestAssemblyFinished(testCases, TestAssembly, totalExecutionTime, testsRun, testsFailed, testsSkipped));
+ }
+
+ return result;
+ }
+
+ protected virtual Task> RunTestCollectionForVersionAsync(VisualStudioInstanceFactory visualStudioInstanceFactory, VisualStudioVersion visualStudioVersion, HashSet completedTestCaseIds, IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource)
+ {
+ if (visualStudioVersion == VisualStudioVersion.Unspecified
+ || !IdeTestCase.IsInstalled(visualStudioVersion))
+ {
+ return RunTestCollectionForUnspecifiedVersionAsync(completedTestCaseIds, messageBus, testCollection, testCases, cancellationTokenSource);
+ }
+
+ DispatcherSynchronizationContext synchronizationContext = null;
+ Dispatcher dispatcher = null;
+ Thread staThread;
+ using (var staThreadStartedEvent = new ManualResetEventSlim(initialState: false))
+ {
+ staThread = new Thread((ThreadStart)(() =>
+ {
+ // All WPF Tests need a DispatcherSynchronizationContext and we don't want to block pending keyboard
+ // or mouse input from the user. So use background priority which is a single level below user input.
+ synchronizationContext = new DispatcherSynchronizationContext();
+ dispatcher = Dispatcher.CurrentDispatcher;
+
+ // xUnit creates its own synchronization context and wraps any existing context so that messages are
+ // still pumped as necessary. So we are safe setting it here, where we are not safe setting it in test.
+ SynchronizationContext.SetSynchronizationContext(synchronizationContext);
+
+ staThreadStartedEvent.Set();
+
+ Dispatcher.Run();
+ }));
+
+ staThread.Name = $"{nameof(IdeTestAssemblyRunner)}";
+ staThread.SetApartmentState(ApartmentState.STA);
+ staThread.Start();
+
+ staThreadStartedEvent.Wait();
+ Debug.Assert(synchronizationContext != null, "Assertion failed: synchronizationContext != null");
+ }
+
+ var taskScheduler = new SynchronizationContextTaskScheduler(synchronizationContext);
+ var task = Task.Factory.StartNew(
+ async () =>
+ {
+ Debug.Assert(SynchronizationContext.Current is DispatcherSynchronizationContext, "Assertion failed: SynchronizationContext.Current is DispatcherSynchronizationContext");
+
+ using (await WpfTestSharedData.Instance.TestSerializationGate.DisposableWaitAsync(CancellationToken.None))
+ {
+ // Just call back into the normal xUnit dispatch process now that we are on an STA Thread with no synchronization context.
+ var invoker = CreateTestCollectionInvoker(visualStudioInstanceFactory, visualStudioVersion, completedTestCaseIds, messageBus, testCollection, testCases, cancellationTokenSource);
+ return await invoker().ConfigureAwait(true);
+ }
+ },
+ cancellationTokenSource.Token,
+ TaskCreationOptions.None,
+ taskScheduler).Unwrap();
+
+ return Task.Run(
+ async () =>
+ {
+ try
+ {
+ return await task.ConfigureAwait(false);
+ }
+ finally
+ {
+ // Make sure to shut down the dispatcher. Certain framework types listed for the dispatcher
+ // shutdown to perform cleanup actions. In the absence of an explicit shutdown, these actions
+ // are delayed and run during AppDomain or process shutdown, where they can lead to crashes of
+ // the test process.
+ dispatcher.InvokeShutdown();
+
+ // Join the STA thread, which ensures shutdown is complete.
+ staThread.Join(HangMitigatingTimeout);
+ }
+ });
+ }
+
+ private async Task> RunTestCollectionForUnspecifiedVersionAsync(HashSet completedTestCaseIds, IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource)
+ {
+ // These tests just run in the current process, but we still need to hook the assembly and collection events
+ // to work correctly in mixed-testing scenarios.
+ var executionMessageSinkFilter = new IpcMessageSink(ExecutionMessageSink, completedTestCaseIds, cancellationTokenSource.Token);
+ using (var runner = new XunitTestAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSinkFilter, ExecutionOptions))
+ {
+ var runSummary = await runner.RunAsync();
+ return Tuple.Create(runSummary, executionMessageSinkFilter.TestAssemblyFinished);
+ }
+ }
+
+ private Func>> CreateTestCollectionInvoker(VisualStudioInstanceFactory visualStudioInstanceFactory, VisualStudioVersion visualStudioVersion, HashSet completedTestCaseIds, IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource)
+ {
+ return async () =>
+ {
+ Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState());
+
+ // Install a COM message filter to handle retry operations when the first attempt fails
+ using (var messageFilter = new MessageFilter())
+ {
+ using (var visualStudioContext = await visualStudioInstanceFactory.GetNewOrUsedInstanceAsync(GetVersion(visualStudioVersion), ImmutableHashSet.Create()).ConfigureAwait(true))
+ {
+ var executionMessageSinkFilter = new IpcMessageSink(ExecutionMessageSink, completedTestCaseIds, cancellationTokenSource.Token);
+ using (var runner = visualStudioContext.Instance.TestInvoker.CreateTestAssemblyRunner(new IpcTestAssembly(TestAssembly), testCases.ToArray(), new IpcMessageSink(DiagnosticMessageSink, new HashSet(), cancellationTokenSource.Token), executionMessageSinkFilter, ExecutionOptions))
+ {
+ var result = runner.RunTestCollection(new IpcMessageBus(messageBus), testCollection, testCases.ToArray());
+ var runSummary = new RunSummary
+ {
+ Total = result.Item1,
+ Failed = result.Item2,
+ Skipped = result.Item3,
+ Time = result.Item4,
+ };
+
+ return Tuple.Create(runSummary, executionMessageSinkFilter.TestAssemblyFinished);
+ }
+ }
+ }
+ };
+ }
+
+ private static Version GetVersion(VisualStudioVersion visualStudioVersion)
+ {
+ switch (visualStudioVersion)
+ {
+ case VisualStudioVersion.VS2012:
+ return new Version(11, 0);
+
+ case VisualStudioVersion.VS2013:
+ return new Version(12, 0);
+
+ case VisualStudioVersion.VS2015:
+ return new Version(14, 0);
+
+ case VisualStudioVersion.VS2017:
+ return new Version(15, 0);
+
+ default:
+ throw new ArgumentException();
+ }
+ }
+
+ private VisualStudioVersion GetVisualStudioVersionForTestCase(IXunitTestCase testCase)
+ {
+ if (testCase is IdeTestCase ideTestCase)
+ {
+ return ideTestCase.VisualStudioVersion;
+ }
+
+ return VisualStudioVersion.Unspecified;
+ }
+
+ private class IpcMessageSink : MarshalByRefObject, IMessageSink
+ {
+ private readonly IMessageSink _messageSink;
+ private readonly CancellationToken _cancellationToken;
+
+ private readonly HashSet _completedTestCaseIds;
+
+ public IpcMessageSink(IMessageSink messageSink, HashSet completedTestCaseIds, CancellationToken cancellationToken)
+ {
+ _messageSink = messageSink;
+ _completedTestCaseIds = completedTestCaseIds;
+ _cancellationToken = cancellationToken;
+ }
+
+ public ITestAssemblyFinished TestAssemblyFinished
+ {
+ get;
+ private set;
+ }
+
+ public bool OnMessage(IMessageSinkMessage message)
+ {
+ if (message is ITestAssemblyFinished testAssemblyFinished)
+ {
+ TestAssemblyFinished = new TestAssemblyFinished(testAssemblyFinished.TestCases.ToArray(), testAssemblyFinished.TestAssembly, testAssemblyFinished.ExecutionTime, testAssemblyFinished.TestsRun, testAssemblyFinished.TestsFailed, testAssemblyFinished.TestsSkipped);
+ return !_cancellationToken.IsCancellationRequested;
+ }
+ else if (message is ITestCaseFinished testCaseFinished)
+ {
+ _completedTestCaseIds.Add(testCaseFinished.TestCase.UniqueID);
+ return !_cancellationToken.IsCancellationRequested;
+ }
+ else if (message is ITestAssemblyStarting
+ || message is ITestCollectionStarting
+ || message is ITestCollectionFinished)
+ {
+ return !_cancellationToken.IsCancellationRequested;
+ }
+
+ return _messageSink.OnMessage(message);
+ }
+ }
+
+ private class IpcMessageBus : MarshalByRefObject, IMessageBus
+ {
+ private readonly IMessageBus _messageBus;
+
+ public IpcMessageBus(IMessageBus messageBus)
+ {
+ _messageBus = messageBus;
+ }
+
+ public void Dispose() => _messageBus.Dispose();
+
+ public bool QueueMessage(IMessageSinkMessage message) => _messageBus.QueueMessage(message);
+ }
+
+ private class IpcTestAssembly : LongLivedMarshalByRefObject, ITestAssembly
+ {
+ private readonly ITestAssembly _testAssembly;
+ private readonly IAssemblyInfo _assembly;
+
+ public IpcTestAssembly(ITestAssembly testAssembly)
+ {
+ _testAssembly = testAssembly;
+ _assembly = new IpcAssemblyInfo(_testAssembly.Assembly);
+ }
+
+ public IAssemblyInfo Assembly => _assembly;
+
+ public string ConfigFileName => _testAssembly.ConfigFileName;
+
+ public void Deserialize(IXunitSerializationInfo info)
+ {
+ _testAssembly.Deserialize(info);
+ }
+
+ public void Serialize(IXunitSerializationInfo info)
+ {
+ _testAssembly.Serialize(info);
+ }
+ }
+
+ private class IpcAssemblyInfo : LongLivedMarshalByRefObject, IAssemblyInfo
+ {
+ private IAssemblyInfo _assemblyInfo;
+
+ public IpcAssemblyInfo(IAssemblyInfo assemblyInfo)
+ {
+ _assemblyInfo = assemblyInfo;
+ }
+
+ public string AssemblyPath => _assemblyInfo.AssemblyPath;
+
+ public string Name => _assemblyInfo.Name;
+
+ public IEnumerable GetCustomAttributes(string assemblyQualifiedAttributeTypeName)
+ {
+ return _assemblyInfo.GetCustomAttributes(assemblyQualifiedAttributeTypeName).ToArray();
+ }
+
+ public ITypeInfo GetType(string typeName)
+ {
+ return _assemblyInfo.GetType(typeName);
+ }
+
+ public IEnumerable GetTypes(bool includePrivateTypes)
+ {
+ return _assemblyInfo.GetTypes(includePrivateTypes).ToArray();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestFramework.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestFramework.cs
new file mode 100644
index 0000000..ac897b4
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestFramework.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System.Reflection;
+ using Xunit.Abstractions;
+ using Xunit.Sdk;
+
+ public class IdeTestFramework : XunitTestFramework
+ {
+ public IdeTestFramework(IMessageSink diagnosticMessageSink)
+ : base(diagnosticMessageSink)
+ {
+ }
+
+ protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
+ {
+ return new IdeTestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestFrameworkExecutor.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestFrameworkExecutor.cs
new file mode 100644
index 0000000..08aad6a
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IdeTestFrameworkExecutor.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System.Collections.Generic;
+ using System.Reflection;
+ using Xunit.Abstractions;
+ using Xunit.Sdk;
+
+ public class IdeTestFrameworkExecutor : XunitTestFrameworkExecutor
+ {
+ public IdeTestFrameworkExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink)
+ : base(assemblyName, sourceInformationProvider, diagnosticMessageSink)
+ {
+ }
+
+ protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
+ {
+ try
+ {
+ using (var assemblyRunner = new IdeTestAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions))
+ {
+ await assemblyRunner.RunAsync();
+ }
+ }
+ catch
+ {
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/InProcessIdeTestAssemblyRunner.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/InProcessIdeTestAssemblyRunner.cs
new file mode 100644
index 0000000..1d355ed
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/InProcessIdeTestAssemblyRunner.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading;
+ using Xunit.Abstractions;
+ using Xunit.Sdk;
+ using Xunit.Threading;
+
+ public class InProcessIdeTestAssemblyRunner : MarshalByRefObject, IDisposable
+ {
+ private readonly TestAssemblyRunner _testAssemblyRunner;
+
+ public InProcessIdeTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
+ {
+ var reconstructedTestCases = testCases.Select(testCase =>
+ {
+ if (testCase is IdeTestCase ideTestCase)
+ {
+ return new IdeTestCase(diagnosticMessageSink, ideTestCase.DefaultMethodDisplay, ideTestCase.TestMethod, ideTestCase.VisualStudioVersion, ideTestCase.TestMethodArguments);
+ }
+
+ return testCase;
+ });
+
+ _testAssemblyRunner = new XunitTestAssemblyRunner(testAssembly, reconstructedTestCases.ToArray(), diagnosticMessageSink, executionMessageSink, executionOptions);
+ }
+
+ public Tuple RunTestCollection(IMessageBus messageBus, ITestCollection testCollection, IXunitTestCase[] testCases)
+ {
+ using (var cancellationTokenSource = new CancellationTokenSource())
+ {
+ var result = _testAssemblyRunner.RunAsync().GetAwaiter().GetResult();
+ return Tuple.Create(result.Total, result.Failed, result.Skipped, result.Time);
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ public override object InitializeLifetimeService()
+ {
+ // This object can live forever
+ return null;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _testAssemblyRunner.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IntegrationHelper.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IntegrationHelper.cs
new file mode 100644
index 0000000..3fdaedb
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/IntegrationHelper.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Runtime.InteropServices;
+ using System.Threading.Tasks;
+ using DTE = EnvDTE.DTE;
+ using IMoniker = Microsoft.VisualStudio.OLE.Interop.IMoniker;
+
+ ///
+ /// Provides some helper functions used by the other classes in the project.
+ ///
+ internal static class IntegrationHelper
+ {
+ ///
+ /// Kills the specified process if it is not null and has not already exited.
+ ///
+ public static void KillProcess(Process process)
+ {
+ if (process != null && !process.HasExited)
+ {
+ process.Kill();
+ }
+ }
+
+ ///
+ /// Kills all processes matching the specified name.
+ ///
+ public static void KillProcess(string processName)
+ {
+ foreach (var process in Process.GetProcessesByName(processName))
+ {
+ KillProcess(process);
+ }
+ }
+
+ /// Locates the DTE object for the specified process.
+ public static DTE TryLocateDteForProcess(Process process)
+ {
+ object dte = null;
+ var monikers = new IMoniker[1];
+
+ NativeMethods.GetRunningObjectTable(0, out var runningObjectTable);
+ runningObjectTable.EnumRunning(out var enumMoniker);
+ NativeMethods.CreateBindCtx(0, out var bindContext);
+
+ do
+ {
+ monikers[0] = null;
+
+ var hresult = enumMoniker.Next(1, monikers, out var monikersFetched);
+
+ if (hresult == VSConstants.S_FALSE)
+ {
+ // There's nothing further to enumerate, so fail
+ return null;
+ }
+ else
+ {
+ Marshal.ThrowExceptionForHR(hresult);
+ }
+
+ var moniker = monikers[0];
+ moniker.GetDisplayName(bindContext, null, out var fullDisplayName);
+
+ // FullDisplayName will look something like: :
+ var displayNameParts = fullDisplayName.Split(':');
+ if (!int.TryParse(displayNameParts.Last(), out var displayNameProcessId))
+ {
+ continue;
+ }
+
+ if (displayNameParts[0].StartsWith("!VisualStudio.DTE", StringComparison.OrdinalIgnoreCase) &&
+ displayNameProcessId == process.Id)
+ {
+ runningObjectTable.GetObject(moniker, out dte);
+ }
+ }
+ while (dte == null);
+
+ return (DTE)dte;
+ }
+
+ public static async Task WaitForNotNullAsync(Func action)
+ where T : class
+ {
+ var result = action();
+
+ while (result == null)
+ {
+ await Task.Yield();
+ result = action();
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/MessageFilter.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/MessageFilter.cs
new file mode 100644
index 0000000..cd5c8a4
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/MessageFilter.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using IMessageFilter = Microsoft.VisualStudio.OLE.Interop.IMessageFilter;
+ using INTERFACEINFO = Microsoft.VisualStudio.OLE.Interop.INTERFACEINFO;
+ using PENDINGMSG = Microsoft.VisualStudio.OLE.Interop.PENDINGMSG;
+ using SERVERCALL = Microsoft.VisualStudio.OLE.Interop.SERVERCALL;
+
+ internal class MessageFilter : IMessageFilter, IDisposable
+ {
+ protected const uint CancelCall = ~0U;
+
+ private readonly MessageFilterSafeHandle _messageFilterRegistration;
+ private readonly TimeSpan _timeout;
+ private readonly TimeSpan _retryDelay;
+
+ public MessageFilter()
+ : this(timeout: TimeSpan.FromSeconds(60), retryDelay: TimeSpan.FromMilliseconds(150))
+ {
+ }
+
+ public MessageFilter(TimeSpan timeout, TimeSpan retryDelay)
+ {
+ _timeout = timeout;
+ _retryDelay = retryDelay;
+ _messageFilterRegistration = MessageFilterSafeHandle.Register(this);
+ }
+
+ public virtual uint HandleInComingCall(uint dwCallType, IntPtr htaskCaller, uint dwTickCount, INTERFACEINFO[] lpInterfaceInfo)
+ {
+ return (uint)SERVERCALL.SERVERCALL_ISHANDLED;
+ }
+
+ public virtual uint RetryRejectedCall(IntPtr htaskCallee, uint dwTickCount, uint dwRejectType)
+ {
+ if ((SERVERCALL)dwRejectType != SERVERCALL.SERVERCALL_RETRYLATER
+ && (SERVERCALL)dwRejectType != SERVERCALL.SERVERCALL_REJECTED)
+ {
+ return CancelCall;
+ }
+
+ if (dwTickCount >= _timeout.TotalMilliseconds)
+ {
+ return CancelCall;
+ }
+
+ return (uint)_retryDelay.TotalMilliseconds;
+ }
+
+ public virtual uint MessagePending(IntPtr htaskCallee, uint dwTickCount, uint dwPendingType)
+ {
+ return (uint)PENDINGMSG.PENDINGMSG_WAITDEFPROCESS;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _messageFilterRegistration.Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/MessageFilterSafeHandle.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/MessageFilterSafeHandle.cs
new file mode 100644
index 0000000..666b8c7
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/MessageFilterSafeHandle.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using Microsoft.Win32.SafeHandles;
+ using IMessageFilter = Microsoft.VisualStudio.OLE.Interop.IMessageFilter;
+
+ internal sealed class MessageFilterSafeHandle : SafeHandleMinusOneIsInvalid
+ {
+ private readonly IntPtr _oldFilter;
+
+ private MessageFilterSafeHandle(IntPtr handle)
+ : base(true)
+ {
+ SetHandle(handle);
+
+ try
+ {
+ if (NativeMethods.CoRegisterMessageFilter(handle, out _oldFilter) != VSConstants.S_OK)
+ {
+ throw new InvalidOperationException("Failed to register a new message filter");
+ }
+ }
+ catch
+ {
+ SetHandleAsInvalid();
+ throw;
+ }
+ }
+
+ public static MessageFilterSafeHandle Register(T messageFilter)
+ where T : IMessageFilter
+ {
+ var handle = Marshal.GetComInterfaceForObject(messageFilter);
+ return new MessageFilterSafeHandle(handle);
+ }
+
+ protected override bool ReleaseHandle()
+ {
+ if (NativeMethods.CoRegisterMessageFilter(_oldFilter, out _) == VSConstants.S_OK)
+ {
+ Marshal.Release(handle);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/NativeMethods.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/NativeMethods.cs
new file mode 100644
index 0000000..5690d40
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/NativeMethods.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using System.Text;
+ using IBindCtx = Microsoft.VisualStudio.OLE.Interop.IBindCtx;
+ using IRunningObjectTable = Microsoft.VisualStudio.OLE.Interop.IRunningObjectTable;
+
+ internal static class NativeMethods
+ {
+ private const string Kernel32 = "kernel32.dll";
+ private const string Ole32 = "ole32.dll";
+ private const string User32 = "User32.dll";
+
+ public const uint GA_PARENT = 1;
+
+ public const uint GW_OWNER = 4;
+
+ public const int HWND_NOTOPMOST = -2;
+ public const int HWND_TOPMOST = -1;
+
+ public const uint SWP_NOSIZE = 0x0001;
+ public const uint SWP_NOMOVE = 0x0002;
+
+ public const uint WM_GETTEXT = 0x000D;
+ public const uint WM_GETTEXTLENGTH = 0x000E;
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi, SetLastError = false)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public delegate bool WNDENUMPROC(IntPtr hWnd, IntPtr lParam);
+
+ [DllImport(Kernel32)]
+ public static extern uint GetCurrentThreadId();
+
+ [DllImport(Ole32, PreserveSig = false)]
+ public static extern void CreateBindCtx(int reserved, [MarshalAs(UnmanagedType.Interface)] out IBindCtx bindContext);
+
+ [DllImport(Ole32, PreserveSig = false)]
+ public static extern void GetRunningObjectTable(int reserved, [MarshalAs(UnmanagedType.Interface)] out IRunningObjectTable runningObjectTable);
+
+ [DllImport(User32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
+
+ [DllImport(User32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool BlockInput([MarshalAs(UnmanagedType.Bool)] bool fBlockIt);
+
+ [DllImport(User32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool EnumWindows([MarshalAs(UnmanagedType.FunctionPtr)] WNDENUMPROC lpEnumFunc, IntPtr lParam);
+
+ [DllImport(User32)]
+ public static extern IntPtr GetAncestor(IntPtr hWnd, uint gaFlags);
+
+ [DllImport(User32)]
+ public static extern IntPtr GetForegroundWindow();
+
+ [DllImport(User32, SetLastError = true)]
+ public static extern IntPtr GetParent(IntPtr hWnd);
+
+ [DllImport(User32, SetLastError = true)]
+ public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
+
+ [DllImport(User32)]
+ public static extern uint GetWindowThreadProcessId(IntPtr hWnd, [Optional] IntPtr lpdwProcessId);
+
+ [DllImport(User32, CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern IntPtr SendMessage(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
+
+ [DllImport(User32, CharSet = CharSet.Unicode, SetLastError = true)]
+ public static extern IntPtr SendMessage(IntPtr hWnd, uint uMsg, IntPtr wParam, [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder lParam);
+
+ [DllImport(User32, SetLastError = true)]
+ public static extern IntPtr SetActiveWindow(IntPtr hWnd);
+
+ [DllImport(User32, SetLastError = true)]
+ public static extern IntPtr SetFocus(IntPtr hWnd);
+
+ [DllImport(User32, SetLastError = false)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
+
+ [DllImport(User32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool SetWindowPos(IntPtr hWnd, [Optional] IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
+
+ [DllImport(Ole32, SetLastError = true)]
+ public static extern int CoRegisterMessageFilter(IntPtr messageFilter, out IntPtr oldMessageFilter);
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/ServiceProvider.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/ServiceProvider.cs
new file mode 100644
index 0000000..ccf0219
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/ServiceProvider.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using IObjectWithSite = Microsoft.VisualStudio.OLE.Interop.IObjectWithSite;
+ using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
+ using IUnknown = stdole.IUnknown;
+
+ internal class ServiceProvider : IServiceProvider, IObjectWithSite
+ {
+ private IOleServiceProvider _serviceProvider;
+
+ public ServiceProvider(IOleServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public object GetService(Type serviceType)
+ {
+ if (serviceType is null)
+ {
+ throw new ArgumentNullException(nameof(serviceType));
+ }
+
+ return GetService(serviceType.GUID);
+ }
+
+ private object GetService(Guid serviceGuid)
+ {
+ if (serviceGuid == typeof(IOleServiceProvider).GUID)
+ {
+ return _serviceProvider;
+ }
+
+ if (serviceGuid == typeof(IObjectWithSite).GUID)
+ {
+ return this;
+ }
+
+ if (_serviceProvider.QueryService(serviceGuid, typeof(IUnknown).GUID, out var obj) < 0)
+ {
+ return null;
+ }
+
+ if (obj == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ try
+ {
+ return Marshal.GetObjectForIUnknown(obj);
+ }
+ finally
+ {
+ Marshal.Release(obj);
+ }
+ }
+
+ void IObjectWithSite.SetSite(object pUnkSite)
+ {
+ if (pUnkSite is IOleServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+ }
+
+ void IObjectWithSite.GetSite(ref Guid riid, out IntPtr ppvSite)
+ {
+ var service = GetService(riid);
+ if (service == null)
+ {
+ Marshal.ThrowExceptionForHR(-2147467262);
+ }
+
+ var unknown = Marshal.GetIUnknownForObject(service);
+ try
+ {
+ Marshal.ThrowExceptionForHR(Marshal.QueryInterface(unknown, ref riid, out ppvSite));
+ }
+ finally
+ {
+ Marshal.Release(unknown);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/Settings.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/Settings.cs
new file mode 100644
index 0000000..4d5aee5
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/Settings.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ internal class Settings
+ {
+ internal static readonly Settings Default = new Settings();
+
+ public string VsRootSuffix => "Exp";
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VSConstants.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VSConstants.cs
new file mode 100644
index 0000000..eb275b2
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VSConstants.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+#pragma warning disable SA1310 // Field names should not contain underscore
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Runtime.InteropServices;
+
+ internal static class VSConstants
+ {
+ public const int S_OK = 0;
+ public const int S_FALSE = 1;
+
+ public const int E_ACCESSDENIED = -2147024891;
+
+ public static readonly Guid GUID_VSStandardCommandSet97 = typeof(VSStd97CmdID).GUID;
+
+ [Guid("5EFC7975-14BC-11CF-9B2B-00AA00573819")]
+ public enum VSStd97CmdID
+ {
+ Exit = 229,
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstance.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstance.cs
new file mode 100644
index 0000000..26eb86d
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstance.cs
@@ -0,0 +1,201 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Collections;
+ using System.Collections.Immutable;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Runtime.Remoting.Channels;
+ using System.Runtime.Remoting.Channels.Ipc;
+ using System.Runtime.Serialization.Formatters;
+ using Microsoft.VisualStudio.IntegrationTestService;
+ using Xunit.InProcess;
+ using Xunit.OutOfProcess;
+ using DTE = EnvDTE.DTE;
+
+ internal class VisualStudioInstance
+ {
+ private readonly IntegrationService _integrationService;
+ private readonly IpcChannel _integrationServiceChannel;
+ private readonly VisualStudio_InProc _inProc;
+
+ public VisualStudioInstance(Process hostProcess, DTE dte, Version version, ImmutableHashSet supportedPackageIds, string installationPath)
+ {
+ HostProcess = hostProcess;
+ Dte = dte;
+ Version = version;
+ SupportedPackageIds = supportedPackageIds;
+ InstallationPath = installationPath;
+
+ if (Debugger.IsAttached)
+ {
+ // If a Visual Studio debugger is attached to the test process, attach it to the instance running
+ // integration tests as well.
+ var debuggerHostDte = GetDebuggerHostDte();
+ int targetProcessId = Process.GetCurrentProcess().Id;
+ var localProcess = debuggerHostDte?.Debugger.LocalProcesses.OfType().FirstOrDefault(p => p.ProcessID == hostProcess.Id);
+ localProcess?.Attach2("Managed");
+ }
+
+ StartRemoteIntegrationService(dte);
+
+ string portName = $"IPC channel client for {HostProcess.Id}";
+ _integrationServiceChannel = new IpcChannel(
+ new Hashtable
+ {
+ { "name", portName },
+ { "portName", portName },
+ },
+ new BinaryClientFormatterSinkProvider(),
+ new BinaryServerFormatterSinkProvider { TypeFilterLevel = TypeFilterLevel.Full });
+
+ ChannelServices.RegisterChannel(_integrationServiceChannel, ensureSecurity: true);
+
+ // Connect to a 'well defined, shouldn't conflict' IPC channel
+ _integrationService = IntegrationService.GetInstanceFromHostProcess(hostProcess);
+
+ // Create marshal-by-ref object that runs in host-process.
+ _inProc = ExecuteInHostProcess(
+ type: typeof(VisualStudio_InProc),
+ methodName: nameof(VisualStudio_InProc.Create));
+
+ // There is a lot of VS initialization code that goes on, so we want to wait for that to 'settle' before
+ // we start executing any actual code.
+ _inProc.WaitForSystemIdle();
+
+ TestInvoker = new TestInvoker_OutOfProc(this);
+
+ // Ensure we are in a known 'good' state by cleaning up anything changed by the previous instance
+ CleanUp();
+ }
+
+ internal DTE Dte
+ {
+ get;
+ }
+
+ internal Process HostProcess
+ {
+ get;
+ }
+
+ public Version Version
+ {
+ get;
+ }
+
+ ///
+ /// Gets the set of Visual Studio packages that are installed into this instance.
+ ///
+ public ImmutableHashSet SupportedPackageIds
+ {
+ get;
+ }
+
+ ///
+ /// Gets the path to the root of this installed version of Visual Studio. This is the folder that contains
+ /// Common7\IDE.
+ ///
+ public string InstallationPath
+ {
+ get;
+ }
+
+ public TestInvoker_OutOfProc TestInvoker
+ {
+ get;
+ }
+
+ public bool IsRunning => !HostProcess.HasExited;
+
+ private static DTE GetDebuggerHostDte()
+ {
+ var currentProcessId = Process.GetCurrentProcess().Id;
+ foreach (var process in Process.GetProcessesByName("devenv"))
+ {
+ var dte = IntegrationHelper.TryLocateDteForProcess(process);
+ if (dte?.Debugger?.DebuggedProcesses?.OfType().Any(p => p.ProcessID == currentProcessId) ?? false)
+ {
+ return dte;
+ }
+ }
+
+ return null;
+ }
+
+ public T ExecuteInHostProcess(Type type, string methodName)
+ {
+ var objectUri = _integrationService.Execute(type.Assembly.Location, type.FullName, methodName) ?? throw new InvalidOperationException("The specified call was expected to return a value.");
+ return (T)Activator.GetObject(typeof(T), $"{_integrationService.BaseUri}/{objectUri}");
+ }
+
+ public void AddCodeBaseDirectory(string directory)
+ => _inProc.AddCodeBaseDirectory(directory);
+
+ public void CleanUp()
+ {
+ }
+
+ public void Close(bool exitHostProcess = true)
+ {
+ if (!IsRunning)
+ {
+ return;
+ }
+
+ CleanUp();
+
+ CloseRemotingService();
+
+ if (exitHostProcess)
+ {
+ CloseHostProcess();
+ }
+ }
+
+ private void CloseHostProcess()
+ {
+ _inProc.Quit();
+ if (!HostProcess.WaitForExit(milliseconds: 10000))
+ {
+ IntegrationHelper.KillProcess(HostProcess);
+ }
+ }
+
+ private void CloseRemotingService()
+ {
+ try
+ {
+ StopRemoteIntegrationService();
+ }
+ finally
+ {
+ if (_integrationServiceChannel != null
+ && ChannelServices.RegisteredChannels.Contains(_integrationServiceChannel))
+ {
+ ChannelServices.UnregisterChannel(_integrationServiceChannel);
+ }
+ }
+ }
+
+ private void StartRemoteIntegrationService(DTE dte)
+ {
+ // We use DTE over RPC to start the integration service. All other DTE calls should happen in the host process.
+ if (dte.Commands.Item(WellKnownCommandNames.IntegrationTestServiceStart).IsAvailable)
+ {
+ dte.ExecuteCommand(WellKnownCommandNames.IntegrationTestServiceStart);
+ }
+ }
+
+ private void StopRemoteIntegrationService()
+ {
+ if (_inProc.IsCommandAvailable(WellKnownCommandNames.IntegrationTestServiceStop))
+ {
+ _inProc.ExecuteCommand(WellKnownCommandNames.IntegrationTestServiceStop);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstanceContext.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstanceContext.cs
new file mode 100644
index 0000000..8532a85
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstanceContext.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+
+ ///
+ /// Represents a wrapper of that is given to a specific test. This should
+ /// be disposed by the test to ensure the test's actions are cleaned up during the test run so the instance is
+ /// usable for the next test.
+ ///
+ internal sealed class VisualStudioInstanceContext : IDisposable
+ {
+ private readonly VisualStudioInstanceFactory _instanceFactory;
+
+ internal VisualStudioInstanceContext(VisualStudioInstance instance, VisualStudioInstanceFactory instanceFactory)
+ {
+ Instance = instance;
+ _instanceFactory = instanceFactory;
+ }
+
+ public VisualStudioInstance Instance
+ {
+ get;
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ Instance.CleanUp();
+ _instanceFactory.NotifyCurrentInstanceContextDisposed(canReuse: true);
+ }
+ catch (Exception)
+ {
+ // If the cleanup process fails, we want to make sure the next test gets a new instance. However,
+ // we still want to raise this exception to fail this test
+ _instanceFactory.NotifyCurrentInstanceContextDisposed(canReuse: false);
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstanceFactory.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstanceFactory.cs
new file mode 100644
index 0000000..66b2a09
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/VisualStudioInstanceFactory.cs
@@ -0,0 +1,401 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Linq;
+ using System.Reflection;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.Setup.Configuration;
+ using Microsoft.Win32;
+ using DTE = EnvDTE.DTE;
+ using File = System.IO.File;
+ using Path = System.IO.Path;
+
+ internal sealed class VisualStudioInstanceFactory : MarshalByRefObject, IDisposable
+ {
+ public static readonly string VsLaunchArgs = $"{(string.IsNullOrWhiteSpace(Settings.Default.VsRootSuffix) ? "/log" : $"/rootsuffix {Settings.Default.VsRootSuffix}")} /log";
+
+ private static readonly Dictionary _installerAssemblies = new Dictionary();
+
+ ///
+ /// The instance that has already been launched by this factory and can be reused.
+ ///
+ private VisualStudioInstance _currentlyRunningInstance;
+
+ private bool _hasCurrentlyActiveContext;
+
+ public VisualStudioInstanceFactory()
+ {
+ if (Process.GetCurrentProcess().ProcessName != "devenv")
+ {
+ AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolveHandler;
+ }
+ }
+
+ // This looks like it is pointless (since we are returning an assembly that is already loaded) but it is actually required.
+ // The BinaryFormatter, when invoking 'HandleReturnMessage', will end up attempting to call 'BinaryAssemblyInfo.GetAssembly()',
+ // which will itself attempt to call 'Assembly.Load()' using the full name of the assembly for the type that is being deserialized.
+ // Depending on the manner in which the assembly was originally loaded, this may end up actually trying to load the assembly a second
+ // time and it can fail if the standard assembly resolution logic fails. This ensures that we 'succeed' this secondary load by returning
+ // the assembly that is already loaded.
+ internal static Assembly AssemblyResolveHandler(object sender, ResolveEventArgs eventArgs)
+ {
+ Debug.WriteLine($"'{eventArgs.RequestingAssembly}' is attempting to resolve '{eventArgs.Name}'");
+ var resolvedAssembly = AppDomain.CurrentDomain.GetAssemblies().Where((assembly) => assembly.FullName.Equals(eventArgs.Name)).SingleOrDefault();
+
+ if (resolvedAssembly != null)
+ {
+ Debug.WriteLine("The assembly was already loaded!");
+ }
+
+ // Support resolving embedded assemblies
+ using (var assemblyStream = typeof(VisualStudioInstanceFactory).Assembly.GetManifestResourceStream(new AssemblyName(eventArgs.Name).Name + ".dll"))
+ using (var memoryStream = new MemoryStream())
+ {
+ if (assemblyStream != null)
+ {
+ assemblyStream.CopyTo(memoryStream);
+ return Assembly.Load(memoryStream.ToArray());
+ }
+ }
+
+ return resolvedAssembly;
+ }
+
+ ///
+ /// Returns a , starting a new instance of Visual Studio if necessary.
+ ///
+ public async Task GetNewOrUsedInstanceAsync(Version version, ImmutableHashSet requiredPackageIds)
+ {
+ ThrowExceptionIfAlreadyHasActiveContext();
+
+ bool shouldStartNewInstance = ShouldStartNewInstance(version, requiredPackageIds);
+ await UpdateCurrentlyRunningInstanceAsync(version, requiredPackageIds, shouldStartNewInstance).ConfigureAwait(false);
+
+ return new VisualStudioInstanceContext(_currentlyRunningInstance, this);
+ }
+
+ internal void NotifyCurrentInstanceContextDisposed(bool canReuse)
+ {
+ ThrowExceptionIfAlreadyHasActiveContext();
+
+ _hasCurrentlyActiveContext = false;
+
+ if (!canReuse)
+ {
+ _currentlyRunningInstance?.Close();
+ _currentlyRunningInstance = null;
+ }
+ }
+
+ private bool ShouldStartNewInstance(Version version, ImmutableHashSet requiredPackageIds)
+ {
+ // We need to start a new instance if:
+ // * The current instance does not exist -or-
+ // * The current instance is not the correct version -or-
+ // * The current instance does not support all the required packages -or-
+ // * The current instance is no longer running
+ return _currentlyRunningInstance == null
+ || _currentlyRunningInstance.Version.Major != version.Major
+ || (!requiredPackageIds.All(id => _currentlyRunningInstance.SupportedPackageIds.Contains(id)))
+ || !_currentlyRunningInstance.IsRunning;
+ }
+
+ private void ThrowExceptionIfAlreadyHasActiveContext()
+ {
+ if (_hasCurrentlyActiveContext)
+ {
+ throw new Exception($"The previous integration test failed to call {nameof(VisualStudioInstanceContext)}.{nameof(Dispose)}. Ensure that test does that to ensure the Visual Studio instance is correctly cleaned up.");
+ }
+ }
+
+ ///
+ /// Starts up a new , shutting down any instances that are already running.
+ ///
+ private async Task UpdateCurrentlyRunningInstanceAsync(Version version, ImmutableHashSet requiredPackageIds, bool shouldStartNewInstance)
+ {
+ Process hostProcess;
+ DTE dte;
+ Version actualVersion;
+ ImmutableHashSet supportedPackageIds;
+ string installationPath;
+
+ if (shouldStartNewInstance)
+ {
+ // We are starting a new instance, so ensure we close the currently running instance, if it exists
+ _currentlyRunningInstance?.Close();
+
+ var instance = LocateVisualStudioInstance(version, requiredPackageIds);
+ supportedPackageIds = instance.Item3;
+ installationPath = instance.Item1;
+ actualVersion = instance.Item2;
+
+ hostProcess = StartNewVisualStudioProcess(installationPath, version);
+
+ // We wait until the DTE instance is up before we're good
+ dte = await IntegrationHelper.WaitForNotNullAsync(() => IntegrationHelper.TryLocateDteForProcess(hostProcess)).ConfigureAwait(true);
+ }
+ else
+ {
+ // We are going to reuse the currently running instance, so ensure that we grab the host Process and Dte
+ // before cleaning up any hooks or remoting services created by the previous instance. We will then
+ // create a new VisualStudioInstance from the previous to ensure that everything is in a 'clean' state.
+ //
+ // We create a new DTE instance in the current context since the COM object could have been separated
+ // from its RCW during the previous test.
+ Debug.Assert(_currentlyRunningInstance != null, "Assertion failed: _currentlyRunningInstance != null");
+
+ hostProcess = _currentlyRunningInstance.HostProcess;
+ dte = await IntegrationHelper.WaitForNotNullAsync(() => IntegrationHelper.TryLocateDteForProcess(hostProcess)).ConfigureAwait(true);
+ actualVersion = _currentlyRunningInstance.Version;
+ supportedPackageIds = _currentlyRunningInstance.SupportedPackageIds;
+ installationPath = _currentlyRunningInstance.InstallationPath;
+
+ _currentlyRunningInstance.Close(exitHostProcess: false);
+ }
+
+ _currentlyRunningInstance = new VisualStudioInstance(hostProcess, dte, actualVersion, supportedPackageIds, installationPath);
+ if (shouldStartNewInstance)
+ {
+ var harnessAssemblyDirectory = Path.GetDirectoryName(typeof(VisualStudioInstanceFactory).Assembly.CodeBase);
+ if (harnessAssemblyDirectory.StartsWith("file:"))
+ {
+ harnessAssemblyDirectory = new Uri(harnessAssemblyDirectory).LocalPath;
+ }
+
+ _currentlyRunningInstance.AddCodeBaseDirectory(harnessAssemblyDirectory);
+ }
+ }
+
+ private static IEnumerable, InstanceState>> EnumerateVisualStudioInstances()
+ {
+ foreach (var result in EnumerateVisualStudioInstancesInRegistry())
+ {
+ yield return Tuple.Create(result.Item1, result.Item2, ImmutableHashSet.Create(), InstanceState.Local | InstanceState.Registered | InstanceState.NoErrors | InstanceState.NoRebootRequired);
+ }
+
+ foreach (ISetupInstance2 result in EnumerateVisualStudioInstancesViaInstaller())
+ {
+ var productDir = Path.GetFullPath(result.GetInstallationPath());
+ var version = Version.Parse(result.GetInstallationVersion());
+ var supportedPackageIds = ImmutableHashSet.CreateRange(result.GetPackages().Select(package => package.GetId()));
+ yield return Tuple.Create(productDir, version, supportedPackageIds, result.GetState());
+ }
+ }
+
+ private static IEnumerable> EnumerateVisualStudioInstancesInRegistry()
+ {
+ using (var software = Registry.LocalMachine.OpenSubKey("SOFTWARE"))
+ using (var microsoft = software.OpenSubKey("Microsoft"))
+ using (var visualStudio = microsoft.OpenSubKey("VisualStudio"))
+ {
+ foreach (string versionKey in visualStudio.GetSubKeyNames())
+ {
+ if (!Version.TryParse(versionKey, out var version))
+ {
+ continue;
+ }
+
+ string path = Registry.GetValue($@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\{versionKey}\Setup\VS", "ProductDir", null) as string;
+ if (string.IsNullOrEmpty(path) || !File.Exists(Path.Combine(path, @"Common7\IDE\devenv.exe")))
+ {
+ continue;
+ }
+
+ yield return Tuple.Create(path, version);
+ }
+ }
+ }
+
+ private static IEnumerable EnumerateVisualStudioInstancesViaInstaller()
+ {
+ var setupConfiguration = new SetupConfiguration();
+
+ var instanceEnumerator = setupConfiguration.EnumAllInstances();
+ var instances = new ISetupInstance[3];
+
+ instanceEnumerator.Next(instances.Length, instances, out var instancesFetched);
+
+ do
+ {
+ for (var index = 0; index < instancesFetched; index++)
+ {
+ yield return instances[index];
+ }
+
+ instanceEnumerator.Next(instances.Length, instances, out instancesFetched);
+ }
+ while (instancesFetched != 0);
+ }
+
+ private static Tuple, InstanceState> LocateVisualStudioInstance(Version version, ImmutableHashSet requiredPackageIds)
+ {
+ var vsInstallDir = Environment.GetEnvironmentVariable("__UNITTESTEXPLORER_VSINSTALLPATH__")
+ ?? Environment.GetEnvironmentVariable("VSAPPIDDIR");
+ if (vsInstallDir != null)
+ {
+ vsInstallDir = Path.GetFullPath(Path.Combine(vsInstallDir, @"..\.."));
+ }
+ else
+ {
+ vsInstallDir = Environment.GetEnvironmentVariable("VSInstallDir");
+ }
+
+ var haveVsInstallDir = !string.IsNullOrEmpty(vsInstallDir);
+
+ if (haveVsInstallDir)
+ {
+ vsInstallDir = Path.GetFullPath(vsInstallDir);
+ vsInstallDir = vsInstallDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ Debug.WriteLine($"An environment variable named 'VSInstallDir' (or equivalent) was found, adding this to the specified requirements. (VSInstallDir: {vsInstallDir})");
+ }
+
+ var instances = EnumerateVisualStudioInstances().Where((instance) =>
+ {
+ var isMatch = true;
+ {
+ isMatch &= version.Major == instance.Item2.Major;
+ isMatch &= instance.Item2 >= version;
+
+ if (haveVsInstallDir && version.Major == 15)
+ {
+ var installationPath = instance.Item1;
+ installationPath = Path.GetFullPath(installationPath);
+ installationPath = installationPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ isMatch &= installationPath.Equals(vsInstallDir, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ return isMatch;
+ });
+
+ var instanceFoundWithInvalidState = false;
+
+ foreach (var instance in instances)
+ {
+ var packages = instance.Item3.Where(package => requiredPackageIds.Contains(package));
+ if (packages.Count() != requiredPackageIds.Count())
+ {
+ continue;
+ }
+
+ const InstanceState minimumRequiredState = InstanceState.Local | InstanceState.Registered;
+
+ var state = instance.Item4;
+
+ if ((state & minimumRequiredState) == minimumRequiredState)
+ {
+ return instance;
+ }
+
+ Debug.WriteLine($"An instance matching the specified requirements but had an invalid state. (State: {state})");
+ instanceFoundWithInvalidState = true;
+ }
+
+ throw new Exception(instanceFoundWithInvalidState ?
+ "An instance matching the specified requirements was found but it was in an invalid state." :
+ "There were no instances of Visual Studio found that match the specified requirements.");
+ }
+
+ private static Process StartNewVisualStudioProcess(string installationPath, Version version)
+ {
+ var vsExeFile = Path.Combine(installationPath, @"Common7\IDE\devenv.exe");
+
+ var installerAssembly = LoadInstallerAssembly(version);
+ var installerType = installerAssembly.GetType("Microsoft.VisualStudio.VsixInstaller.Installer");
+ var installMethod = installerType.GetMethod("Install");
+
+ var install = (Action, string, string>)Delegate.CreateDelegate(typeof(Action, string, string>), installMethod);
+
+ var temporaryFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Assert.False(Directory.Exists(temporaryFolder));
+ Directory.CreateDirectory(temporaryFolder);
+
+ var integrationTestServiceExtension = ExtractIntegrationTestServiceExtension(temporaryFolder);
+ var extensions = new[] { integrationTestServiceExtension };
+ var rootSuffix = Settings.Default.VsRootSuffix;
+
+ try
+ {
+ install(extensions, installationPath, rootSuffix);
+ }
+ finally
+ {
+ File.Delete(integrationTestServiceExtension);
+ Directory.Delete(temporaryFolder);
+ }
+
+ // BUG: Currently building with /p:DeployExtension=true does not always cause the MEF cache to recompose...
+ // So, run clearcache and updateconfiguration to workaround https://devdiv.visualstudio.com/DevDiv/_workitems?id=385351.
+ if (version.Major >= 12)
+ {
+ Process.Start(vsExeFile, $"/clearcache {VsLaunchArgs}").WaitForExit();
+ }
+
+ Process.Start(vsExeFile, $"/updateconfiguration {VsLaunchArgs}").WaitForExit();
+ Process.Start(vsExeFile, $"/resetsettings General.vssettings /command \"File.Exit\" {VsLaunchArgs}").WaitForExit();
+
+ // Make sure we kill any leftover processes spawned by the host
+ IntegrationHelper.KillProcess("DbgCLR");
+ IntegrationHelper.KillProcess("VsJITDebugger");
+ IntegrationHelper.KillProcess("dexplore");
+
+ var process = Process.Start(vsExeFile, VsLaunchArgs);
+ Debug.WriteLine($"Launched a new instance of Visual Studio. (ID: {process.Id})");
+
+ return process;
+ }
+
+ private static Assembly LoadInstallerAssembly(Version version)
+ {
+ version = new Version(version.Major, 0, 0, 0);
+
+ lock (_installerAssemblies)
+ {
+ if (!_installerAssemblies.TryGetValue(version, out var assembly))
+ {
+ var installerAssemblyFile = $"Microsoft.VisualStudio.VsixInstaller.{version.Major}.dll";
+ using (var assemblyStream = typeof(VisualStudioInstanceFactory).Assembly.GetManifestResourceStream(installerAssemblyFile))
+ using (var memoryStream = new MemoryStream())
+ {
+ assemblyStream.CopyTo(memoryStream);
+ assembly = Assembly.Load(memoryStream.ToArray());
+ _installerAssemblies[version] = assembly;
+ }
+ }
+
+ return assembly;
+ }
+ }
+
+ private static string ExtractIntegrationTestServiceExtension(string temporaryFolder)
+ {
+ var extensionFileName = "Microsoft.VisualStudio.IntegrationTestService.vsix";
+ var path = Path.Combine(temporaryFolder, extensionFileName);
+ using (var resourceStream = typeof(VisualStudioInstanceFactory).Assembly.GetManifestResourceStream(extensionFileName))
+ using (var writerStream = File.Open(path, FileMode.CreateNew, FileAccess.Write))
+ {
+ resourceStream.CopyTo(writerStream);
+ }
+
+ return path;
+ }
+
+ public void Dispose()
+ {
+ _currentlyRunningInstance?.Close();
+ _currentlyRunningInstance = null;
+
+ // We want to make sure everybody cleaned up their contexts by the end of everything
+ ThrowExceptionIfAlreadyHasActiveContext();
+
+ AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolveHandler;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/WellKnownCommandNames.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/WellKnownCommandNames.cs
new file mode 100644
index 0000000..956a91e
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Harness/WellKnownCommandNames.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Harness
+{
+ public static class WellKnownCommandNames
+ {
+ public const string IntegrationTestServiceStart = "IntegrationTestService.Start";
+ public const string IntegrationTestServiceStop = "IntegrationTestService.Stop";
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/IdeFactAttribute.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/IdeFactAttribute.cs
new file mode 100644
index 0000000..4868fa8
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/IdeFactAttribute.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit
+{
+ using System;
+ using Xunit.Sdk;
+
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ [XunitTestCaseDiscoverer("Xunit.Threading.IdeFactDiscoverer", "Microsoft.VisualStudio.Extensibility.Testing.Xunit")]
+ public class IdeFactAttribute : FactAttribute
+ {
+ public IdeFactAttribute()
+ {
+ MinVersion = VisualStudioVersion.VS2012;
+ MaxVersion = VisualStudioVersion.VS2017;
+ }
+
+ public VisualStudioVersion MinVersion
+ {
+ get;
+ set;
+ }
+
+ public VisualStudioVersion MaxVersion
+ {
+ get;
+ set;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/InProcComponent.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/InProcComponent.cs
new file mode 100644
index 0000000..4250018
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/InProcComponent.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.InProcess
+{
+ using System;
+ using System.Windows;
+ using System.Windows.Threading;
+ using Microsoft.VisualStudio.Shell.Interop;
+ using Xunit.Harness;
+ using DTE = EnvDTE.DTE;
+
+ ///
+ /// Base class for all components that run inside of the Visual Studio process.
+ ///
+ /// - Every in-proc component should provide a public, static, parameterless "Create" method.
+ /// This will be called to construct the component in the VS process.
+ /// - Public methods on in-proc components should be instance methods to ensure that they are
+ /// marshalled properly and execute in the VS process. Static methods will execute in the process
+ /// in which they are called.
+ ///
+ ///
+ internal abstract class InProcComponent : MarshalByRefObject
+ {
+ protected InProcComponent()
+ {
+ }
+
+ private static Dispatcher CurrentApplicationDispatcher
+ => Application.Current.Dispatcher;
+
+ protected static void BeginInvokeOnUIThread(Action action)
+ => CurrentApplicationDispatcher.BeginInvoke(action, DispatcherPriority.Background);
+
+ protected static void InvokeOnUIThread(Action action)
+ => CurrentApplicationDispatcher.Invoke(action, DispatcherPriority.Background);
+
+ protected static T InvokeOnUIThread(Func action)
+ => CurrentApplicationDispatcher.Invoke(action, DispatcherPriority.Background);
+
+ protected static TInterface GetGlobalService()
+ where TService : class
+ where TInterface : class
+ => InvokeOnUIThread(() => (TInterface)new OleServiceProvider(GetDTE()).GetService(typeof(TService)));
+
+ protected static DTE GetDTE()
+ => InvokeOnUIThread(() => (DTE)GlobalServiceProvider.ServiceProvider.GetService(typeof(SDTE)));
+
+ protected static bool IsCommandAvailable(string commandName)
+ => GetDTE().Commands.Item(commandName).IsAvailable;
+
+ protected static void ExecuteCommand(string commandName, string args = "")
+ => GetDTE().ExecuteCommand(commandName, args);
+
+ ///
+ /// Waiting for the application to 'idle' means that it is done pumping messages (including WM_PAINT).
+ ///
+ protected static void WaitForApplicationIdle()
+ => CurrentApplicationDispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
+
+ protected static void WaitForSystemIdle()
+ => CurrentApplicationDispatcher.Invoke(() => { }, DispatcherPriority.SystemIdle);
+
+ // Ensure InProcComponents live forever
+ public override object InitializeLifetimeService() => null;
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/OleServiceProvider.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/OleServiceProvider.cs
new file mode 100644
index 0000000..fdefa3a
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/OleServiceProvider.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.InProcess
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
+ using IUnknown = stdole.IUnknown;
+
+ internal sealed class OleServiceProvider : IServiceProvider
+ {
+ private readonly IOleServiceProvider _serviceProvider;
+
+ public OleServiceProvider(IOleServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+ }
+
+ public OleServiceProvider(EnvDTE.DTE dte)
+ : this((IOleServiceProvider)dte)
+ {
+ }
+
+ public object GetService(Type serviceType)
+ {
+ if (serviceType is null)
+ {
+ throw new ArgumentNullException(nameof(serviceType));
+ }
+
+ if (serviceType.GUID == typeof(IOleServiceProvider).GUID)
+ {
+ return _serviceProvider;
+ }
+
+ Marshal.ThrowExceptionForHR(_serviceProvider.QueryService(serviceType.GUID, typeof(IUnknown).GUID, out var service));
+ try
+ {
+ return Marshal.GetObjectForIUnknown(service);
+ }
+ finally
+ {
+ Marshal.Release(service);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/TestInvoker_InProc.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/TestInvoker_InProc.cs
new file mode 100644
index 0000000..d8694b3
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/TestInvoker_InProc.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.InProcess
+{
+ using System;
+ using System.Diagnostics;
+ using System.Reflection;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using System.Windows.Threading;
+ using Xunit.Abstractions;
+ using Xunit.Harness;
+ using Xunit.Sdk;
+ using Xunit.Threading;
+
+ internal class TestInvoker_InProc : InProcComponent
+ {
+ private TestInvoker_InProc()
+ {
+ AppDomain.CurrentDomain.AssemblyResolve += VisualStudioInstanceFactory.AssemblyResolveHandler;
+ }
+
+ public static TestInvoker_InProc Create()
+ => new TestInvoker_InProc();
+
+ public void LoadAssembly(string codeBase)
+ {
+ var assembly = Assembly.LoadFrom(codeBase);
+ }
+
+ public InProcessIdeTestAssemblyRunner CreateTestAssemblyRunner(ITestAssembly testAssembly, IXunitTestCase[] testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
+ {
+ return new InProcessIdeTestAssemblyRunner(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions);
+ }
+
+ public Tuple InvokeTest(
+ ITest test,
+ IMessageBus messageBus,
+ Type testClass,
+ object[] constructorArguments,
+ MethodInfo testMethod,
+ object[] testMethodArguments)
+ {
+ var aggregator = new ExceptionAggregator();
+ var beforeAfterAttributes = new BeforeAfterTestAttribute[0];
+ var cancellationTokenSource = new CancellationTokenSource();
+
+ var synchronizationContext = new DispatcherSynchronizationContext(Application.Current.Dispatcher, DispatcherPriority.Background);
+ var result = Task.Factory.StartNew(
+ async () =>
+ {
+ try
+ {
+ var invoker = new XunitTestInvoker(
+ test,
+ messageBus,
+ testClass,
+ constructorArguments,
+ testMethod,
+ testMethodArguments,
+ beforeAfterAttributes,
+ aggregator,
+ cancellationTokenSource);
+ return await invoker.RunAsync();
+ }
+ catch (Exception)
+ {
+ Debugger.Launch();
+ throw;
+ }
+ },
+ CancellationToken.None,
+ TaskCreationOptions.None,
+ new SynchronizationContextTaskScheduler(synchronizationContext)).Unwrap().GetAwaiter().GetResult();
+
+ return Tuple.Create(result, aggregator.ToException());
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/VisualStudio_InProc.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/VisualStudio_InProc.cs
new file mode 100644
index 0000000..e4bf1a6
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/InProcess/VisualStudio_InProc.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.InProcess
+{
+ using System;
+ using System.Reflection;
+ using System.Runtime.InteropServices;
+ using Xunit.Harness;
+ using File = System.IO.File;
+ using IVsUIShell = Microsoft.VisualStudio.Shell.Interop.IVsUIShell;
+ using OLECMDEXECOPT = Microsoft.VisualStudio.OLE.Interop.OLECMDEXECOPT;
+ using Path = System.IO.Path;
+ using SVsUIShell = Microsoft.VisualStudio.Shell.Interop.SVsUIShell;
+
+ internal partial class VisualStudio_InProc : InProcComponent
+ {
+ private VisualStudio_InProc()
+ {
+ }
+
+ public static VisualStudio_InProc Create()
+ => new VisualStudio_InProc();
+
+ public new void WaitForSystemIdle()
+ => InProcComponent.WaitForSystemIdle();
+
+ public new bool IsCommandAvailable(string commandName)
+ => InProcComponent.IsCommandAvailable(commandName);
+
+ public new void ExecuteCommand(string commandName, string args = "")
+ => InProcComponent.ExecuteCommand(commandName, args);
+
+ public void AddCodeBaseDirectory(string directory)
+ {
+ AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
+ {
+ string path = Path.Combine(directory, new AssemblyName(e.Name).Name + ".dll");
+ if (File.Exists(path))
+ {
+ return Assembly.LoadFrom(path);
+ }
+
+ return null;
+ };
+ }
+
+ public void Quit()
+ {
+ BeginInvokeOnUIThread(() =>
+ {
+ var shell = GetGlobalService();
+ var cmdGroup = VSConstants.GUID_VSStandardCommandSet97;
+ var cmdId = VSConstants.VSStd97CmdID.Exit;
+ var cmdExecOpt = OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER;
+ Marshal.ThrowExceptionForHR(shell.PostExecCommand(cmdGroup, (uint)cmdId, (uint)cmdExecOpt, pvaIn: null));
+ });
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Microsoft.VisualStudio.Extensibility.Testing.Xunit.csproj b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Microsoft.VisualStudio.Extensibility.Testing.Xunit.csproj
new file mode 100644
index 0000000..f840553
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Microsoft.VisualStudio.Extensibility.Testing.Xunit.csproj
@@ -0,0 +1,52 @@
+
+
+
+
+ net46
+ Xunit
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/OutOfProcess/OutOfProcComponent.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/OutOfProcess/OutOfProcComponent.cs
new file mode 100644
index 0000000..469fdad
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/OutOfProcess/OutOfProcComponent.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.OutOfProcess
+{
+ using Xunit.Harness;
+ using Xunit.InProcess;
+
+ internal abstract class OutOfProcComponent
+ {
+ protected OutOfProcComponent(VisualStudioInstance visualStudioInstance)
+ {
+ VisualStudioInstance = visualStudioInstance;
+ }
+
+ protected VisualStudioInstance VisualStudioInstance
+ {
+ get;
+ }
+
+ internal static TInProcComponent CreateInProcComponent(VisualStudioInstance visualStudioInstance)
+ where TInProcComponent : InProcComponent
+ {
+ return visualStudioInstance.ExecuteInHostProcess(type: typeof(TInProcComponent), methodName: "Create");
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/OutOfProcess/TestInvoker_OutOfProc.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/OutOfProcess/TestInvoker_OutOfProc.cs
new file mode 100644
index 0000000..f71bf78
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/OutOfProcess/TestInvoker_OutOfProc.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.OutOfProcess
+{
+ using System;
+ using System.Linq;
+ using System.Reflection;
+ using Xunit.Abstractions;
+ using Xunit.Harness;
+ using Xunit.InProcess;
+ using Xunit.Sdk;
+
+ internal class TestInvoker_OutOfProc : OutOfProcComponent
+ {
+ internal TestInvoker_OutOfProc(VisualStudioInstance visualStudioInstance)
+ : base(visualStudioInstance)
+ {
+ TestInvokerInProc = CreateInProcComponent(visualStudioInstance);
+ }
+
+ internal TestInvoker_InProc TestInvokerInProc
+ {
+ get;
+ }
+
+ public void LoadAssembly(string codeBase)
+ {
+ TestInvokerInProc.LoadAssembly(codeBase);
+ }
+
+ public InProcessIdeTestAssemblyRunner CreateTestAssemblyRunner(ITestAssembly testAssembly, IXunitTestCase[] testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
+ {
+ return TestInvokerInProc.CreateTestAssemblyRunner(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions);
+ }
+
+ public Tuple InvokeTest(
+ ITest test,
+ IMessageBus messageBus,
+ Type testClass,
+ object[] constructorArguments,
+ MethodInfo testMethod,
+ object[] testMethodArguments)
+ {
+ if (constructorArguments != null)
+ {
+ if (constructorArguments.OfType().Any())
+ {
+ constructorArguments = (object[])constructorArguments.Clone();
+ for (int i = 0; i < constructorArguments.Length; i++)
+ {
+ if (constructorArguments[i] is ITestOutputHelper testOutputHelper)
+ {
+ constructorArguments[i] = new TestOutputHelperWrapper(testOutputHelper);
+ }
+ }
+ }
+ }
+
+ return TestInvokerInProc.InvokeTest(
+ test,
+ messageBus,
+ testClass,
+ constructorArguments,
+ testMethod,
+ testMethodArguments);
+ }
+
+ private class TestOutputHelperWrapper : MarshalByRefObject, ITestOutputHelper
+ {
+ private readonly ITestOutputHelper _testOutputHelper;
+
+ public TestOutputHelperWrapper(ITestOutputHelper testOutputHelper)
+ {
+ _testOutputHelper = testOutputHelper;
+ }
+
+ public void WriteLine(string message)
+ {
+ _testOutputHelper.WriteLine(message);
+ }
+
+ public void WriteLine(string format, params object[] args)
+ {
+ _testOutputHelper.WriteLine(format, args);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..4c72974
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: CLSCompliant(false)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/ExceptionUtilities.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/ExceptionUtilities.cs
new file mode 100644
index 0000000..db6648b
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/ExceptionUtilities.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Threading
+{
+ using System;
+
+ internal static class ExceptionUtilities
+ {
+ internal static Exception Unreachable
+ => new InvalidOperationException("This program location is thought to be unreachable.");
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeFactDiscoverer.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeFactDiscoverer.cs
new file mode 100644
index 0000000..52461d6
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeFactDiscoverer.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Threading
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using Xunit.Abstractions;
+ using Xunit.Harness;
+ using Xunit.Sdk;
+
+ public class IdeFactDiscoverer : IXunitTestCaseDiscoverer
+ {
+ private readonly IMessageSink _diagnosticMessageSink;
+
+ public IdeFactDiscoverer(IMessageSink diagnosticMessageSink)
+ {
+ _diagnosticMessageSink = diagnosticMessageSink;
+ }
+
+ public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
+ {
+ if (!testMethod.Method.GetParameters().Any())
+ {
+ if (!testMethod.Method.IsGenericMethodDefinition)
+ {
+ var testCases = new List();
+ foreach (var supportedVersion in GetSupportedVersions(factAttribute))
+ {
+ yield return new IdeTestCase(_diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, supportedVersion);
+ }
+ }
+ else
+ {
+ yield return new ExecutionErrorTestCase(_diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, "[IdeFact] methods are not allowed to be generic.");
+ }
+ }
+ else
+ {
+ yield return new ExecutionErrorTestCase(_diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, "[IdeFact] methods are not allowed to have parameters. Did you mean to use [IdeTheory]?");
+ }
+ }
+
+ private IEnumerable GetSupportedVersions(IAttributeInfo theoryAttribute)
+ {
+ var minVersion = theoryAttribute.GetNamedArgument(nameof(IdeFactAttribute.MinVersion));
+ minVersion = minVersion == VisualStudioVersion.Unspecified ? VisualStudioVersion.VS2012 : minVersion;
+
+ var maxVersion = theoryAttribute.GetNamedArgument(nameof(IdeFactAttribute.MaxVersion));
+ maxVersion = maxVersion == VisualStudioVersion.Unspecified ? VisualStudioVersion.VS2017 : maxVersion;
+
+ for (var version = minVersion; version <= maxVersion; version++)
+ {
+ yield return version;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeTestCase.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeTestCase.cs
new file mode 100644
index 0000000..1ec458d
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeTestCase.cs
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Threading
+{
+ using System;
+ using System.ComponentModel;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Win32;
+ using Xunit.Abstractions;
+ using Xunit.Harness;
+ using Xunit.Sdk;
+
+ public sealed class IdeTestCase : XunitTestCase
+ {
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("Called by the deserializer; should only be called by deriving classes for deserialization purposes", error: true)]
+ public IdeTestCase()
+ {
+ }
+
+ public IdeTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, ITestMethod testMethod, VisualStudioVersion visualStudioVersion, object[] testMethodArguments = null)
+ : base(diagnosticMessageSink, defaultMethodDisplay, testMethod, testMethodArguments)
+ {
+ SharedData = WpfTestSharedData.Instance;
+ VisualStudioVersion = visualStudioVersion;
+
+ if (!IsInstalled(visualStudioVersion))
+ {
+ SkipReason = $"{visualStudioVersion} is not installed";
+ }
+ }
+
+ public VisualStudioVersion VisualStudioVersion
+ {
+ get;
+ private set;
+ }
+
+ public new TestMethodDisplay DefaultMethodDisplay => base.DefaultMethodDisplay;
+
+ public WpfTestSharedData SharedData
+ {
+ get;
+ private set;
+ }
+
+ protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName)
+ {
+ var baseName = base.GetDisplayName(factAttribute, displayName);
+ return $"{baseName} ({VisualStudioVersion})";
+ }
+
+ protected override string GetUniqueID()
+ {
+ return $"{base.GetUniqueID()}_{VisualStudioVersion}";
+ }
+
+ public override Task RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
+ {
+ TestCaseRunner runner;
+ if (!string.IsNullOrEmpty(SkipReason))
+ {
+ // Use XunitTestCaseRunner so the skip gets reported without trying to open VS
+ runner = new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource);
+ }
+ else
+ {
+ runner = new IdeTestCaseRunner(SharedData, VisualStudioVersion, this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource);
+ }
+
+ return runner.RunAsync();
+ }
+
+ public override void Serialize(IXunitSerializationInfo data)
+ {
+ base.Serialize(data);
+ data.AddValue(nameof(VisualStudioVersion), (int)VisualStudioVersion);
+ data.AddValue(nameof(SkipReason), SkipReason);
+ }
+
+ public override void Deserialize(IXunitSerializationInfo data)
+ {
+ base.Deserialize(data);
+ VisualStudioVersion = (VisualStudioVersion)data.GetValue(nameof(VisualStudioVersion));
+ SkipReason = data.GetValue(nameof(SkipReason));
+ SharedData = WpfTestSharedData.Instance;
+ }
+
+ internal static bool IsInstalled(VisualStudioVersion visualStudioVersion)
+ {
+ string dteKey;
+
+ switch (visualStudioVersion)
+ {
+ case VisualStudioVersion.VS2012:
+ dteKey = "VisualStudio.DTE.11.0";
+ break;
+
+ case VisualStudioVersion.VS2013:
+ dteKey = "VisualStudio.DTE.12.0";
+ break;
+
+ case VisualStudioVersion.VS2015:
+ dteKey = "VisualStudio.DTE.14.0";
+ break;
+
+ case VisualStudioVersion.VS2017:
+ dteKey = "VisualStudio.DTE.15.0";
+ break;
+
+ default:
+ throw new ArgumentException();
+ }
+
+ using (var key = Registry.ClassesRoot.OpenSubKey(dteKey))
+ {
+ return key != null;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeTestCaseRunner.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeTestCaseRunner.cs
new file mode 100644
index 0000000..4ea40ce
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/IdeTestCaseRunner.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Threading
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Reflection;
+ using System.Threading;
+ using Xunit.Abstractions;
+ using Xunit.Harness;
+ using Xunit.Sdk;
+
+ public sealed class IdeTestCaseRunner : XunitTestCaseRunner
+ {
+ public IdeTestCaseRunner(
+ WpfTestSharedData sharedData,
+ VisualStudioVersion visualStudioVersion,
+ IXunitTestCase testCase,
+ string displayName,
+ string skipReason,
+ object[] constructorArguments,
+ object[] testMethodArguments,
+ IMessageBus messageBus,
+ ExceptionAggregator aggregator,
+ CancellationTokenSource cancellationTokenSource)
+ : base(testCase, displayName, skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource)
+ {
+ SharedData = sharedData;
+ VisualStudioVersion = visualStudioVersion;
+ }
+
+ public WpfTestSharedData SharedData
+ {
+ get;
+ }
+
+ public VisualStudioVersion VisualStudioVersion
+ {
+ get;
+ }
+
+ protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, object[] testMethodArguments, string skipReason, IReadOnlyList beforeAfterAttributes, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
+ {
+ if (Process.GetCurrentProcess().ProcessName == "devenv")
+ {
+ // We are already running inside Visual Studio
+ // TODO: Verify version under test
+ return new InProcessIdeTestRunner(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource);
+ }
+ else
+ {
+ throw new NotSupportedException($"{nameof(IdeFactAttribute)} can only be used with the {nameof(IdeTestFramework)} test framework");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/InProcessIdeTestRunner.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/InProcessIdeTestRunner.cs
new file mode 100644
index 0000000..5467d49
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/InProcessIdeTestRunner.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Threading
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Reflection;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Windows;
+ using System.Windows.Threading;
+ using Xunit.Abstractions;
+ using Xunit.Sdk;
+
+ public class InProcessIdeTestRunner : XunitTestRunner
+ {
+ public InProcessIdeTestRunner(ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, object[] testMethodArguments, string skipReason, IReadOnlyList beforeAfterAttributes, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
+ : base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
+ {
+ }
+
+ protected override Task InvokeTestMethodAsync(ExceptionAggregator aggregator)
+ {
+ var synchronizationContext = new DispatcherSynchronizationContext(Application.Current.Dispatcher, DispatcherPriority.Background);
+ var taskScheduler = new SynchronizationContextTaskScheduler(synchronizationContext);
+ return Task.Factory.StartNew(
+ () => base.InvokeTestMethodAsync(aggregator),
+ CancellationToken.None,
+ TaskCreationOptions.None,
+ taskScheduler).Unwrap();
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/SemaphoreExtensions.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/SemaphoreExtensions.cs
new file mode 100644
index 0000000..1f3035a
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/SemaphoreExtensions.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Threading
+{
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ internal static class SemaphoreExtensions
+ {
+ public static SemaphoreDisposer DisposableWait(this Semaphore semaphore, CancellationToken cancellationToken)
+ {
+ if (cancellationToken.CanBeCanceled)
+ {
+ int signalledIndex = WaitHandle.WaitAny(new[] { semaphore, cancellationToken.WaitHandle });
+ if (signalledIndex != 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ throw ExceptionUtilities.Unreachable;
+ }
+ }
+ else
+ {
+ semaphore.WaitOne();
+ }
+
+ return new SemaphoreDisposer(semaphore);
+ }
+
+ public static Task DisposableWaitAsync(this Semaphore semaphore, CancellationToken cancellationToken)
+ {
+ return Task.Factory.StartNew(
+ () => DisposableWait(semaphore, cancellationToken),
+ cancellationToken,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+ }
+
+ internal struct SemaphoreDisposer : IDisposable
+ {
+ private readonly Semaphore _semaphore;
+
+ public SemaphoreDisposer(Semaphore semaphore)
+ {
+ _semaphore = semaphore;
+ }
+
+ public void Dispose()
+ {
+ _semaphore.Release();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/SynchronizationContextTaskScheduler.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/SynchronizationContextTaskScheduler.cs
new file mode 100644
index 0000000..4ff1d9a
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/SynchronizationContextTaskScheduler.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Threading
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ // Based on CoreCLR's implementation of the TaskScheduler they return from TaskScheduler.FromCurrentSynchronizationContext
+ public class SynchronizationContextTaskScheduler : TaskScheduler
+ {
+ private readonly SendOrPostCallback _postCallback;
+ private readonly SynchronizationContext _synchronizationContext;
+
+ public SynchronizationContextTaskScheduler(SynchronizationContext synchronizationContext)
+ {
+ _postCallback = new SendOrPostCallback(PostCallback);
+ _synchronizationContext = synchronizationContext ?? throw new ArgumentNullException(nameof(synchronizationContext));
+ }
+
+ public override int MaximumConcurrencyLevel => 1;
+
+ protected override void QueueTask(Task task)
+ {
+ _synchronizationContext.Post(_postCallback, task);
+ }
+
+ protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
+ {
+ if (SynchronizationContext.Current == _synchronizationContext)
+ {
+ return TryExecuteTask(task);
+ }
+
+ return false;
+ }
+
+ protected override IEnumerable GetScheduledTasks()
+ {
+ return null;
+ }
+
+ private void PostCallback(object obj)
+ {
+ TryExecuteTask((Task)obj);
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/WpfTestSharedData.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/WpfTestSharedData.cs
new file mode 100644
index 0000000..c7a3921
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/Threading/WpfTestSharedData.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit.Threading
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading;
+
+ [Serializable]
+ public sealed class WpfTestSharedData
+ {
+ internal static readonly WpfTestSharedData Instance = new WpfTestSharedData();
+
+ ///
+ /// The name of a used to ensure that only a single
+ /// -attributed test runs at once. This requirement must be made because,
+ /// currently, 's logic sets various static state before a method runs. If two tests
+ /// run interleaved on the same scheduler (i.e. if one yields with an await) then all bets are off.
+ ///
+ internal static readonly Guid TestSerializationGateName = Guid.NewGuid();
+
+ ///
+ /// Holds the last 10 test cases executed: more recent test cases will occur later in the
+ /// list. Useful for debugging deadlocks that occur because state leak between runs.
+ ///
+ private readonly List _recentTestCases = new List();
+
+ private Semaphore _testSerializationGate = new Semaphore(1, 1, TestSerializationGateName.ToString("N"));
+
+ private WpfTestSharedData()
+ {
+ }
+
+ public Semaphore TestSerializationGate => _testSerializationGate;
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/VisualStudioVersion.cs b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/VisualStudioVersion.cs
new file mode 100644
index 0000000..bfac326
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Extensibility.Testing.Xunit/VisualStudioVersion.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Xunit
+{
+ public enum VisualStudioVersion
+ {
+ Unspecified,
+ VS2012,
+ VS2013,
+ VS2015,
+ VS2017,
+ }
+}
diff --git a/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationService.cs b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationService.cs
new file mode 100644
index 0000000..6ce9efc
--- /dev/null
+++ b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationService.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Microsoft.VisualStudio.IntegrationTestService
+{
+ using System;
+ using System.Collections.Concurrent;
+ using System.Diagnostics;
+ using System.Reflection;
+ using System.Runtime.Remoting;
+
+ ///
+ /// Provides a means of executing code in the Visual Studio host process.
+ ///
+ ///
+ /// This object exists in the Visual Studio host and is marshaled across the process boundary.
+ ///
+ public class IntegrationService : MarshalByRefObject
+ {
+ private readonly ConcurrentDictionary _marshaledObjects = new ConcurrentDictionary();
+
+ public IntegrationService()
+ {
+ PortName = GetPortName(Process.GetCurrentProcess().Id);
+ BaseUri = "ipc://" + PortName;
+ }
+
+ public string PortName
+ {
+ get;
+ }
+
+ ///
+ /// Gets the base Uri of the service. This resolves to a string such as ipc://IntegrationService_{HostProcessId}".
+ ///
+ public string BaseUri
+ {
+ get;
+ }
+
+ private static string GetPortName(int hostProcessId)
+ {
+ // Make the channel name well-known by using a static base and appending the process ID of the host
+ return $"{nameof(IntegrationService)}_{{{hostProcessId}}}";
+ }
+
+ public static IntegrationService GetInstanceFromHostProcess(Process hostProcess)
+ {
+ var uri = $"ipc://{GetPortName(hostProcess.Id)}/{typeof(IntegrationService).FullName}";
+ return (IntegrationService)Activator.GetObject(typeof(IntegrationService), uri);
+ }
+
+ public string Execute(string assemblyFilePath, string typeFullName, string methodName)
+ {
+ var assembly = Assembly.LoadFrom(assemblyFilePath);
+ var type = assembly.GetType(typeFullName);
+ var methodInfo = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static);
+ var result = methodInfo.Invoke(null, null);
+
+ if (methodInfo.ReturnType == typeof(void))
+ {
+ return null;
+ }
+
+ // Create a unique URL for each object returned, so that we can communicate with each object individually
+ var resultType = result.GetType();
+ var marshallableResult = (MarshalByRefObject)result;
+ var objectUri = $"{resultType.FullName}_{Guid.NewGuid()}";
+ var marshalledObject = RemotingServices.Marshal(marshallableResult, objectUri, resultType);
+
+ if (!_marshaledObjects.TryAdd(objectUri, marshalledObject))
+ {
+ throw new InvalidOperationException($"An object with the specified URI has already been marshaled. (URI: {objectUri})");
+ }
+
+ return objectUri;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServiceCommands.cs b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServiceCommands.cs
new file mode 100644
index 0000000..facc864
--- /dev/null
+++ b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServiceCommands.cs
@@ -0,0 +1,137 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Microsoft.VisualStudio.IntegrationTestService
+{
+ using System;
+ using System.Collections;
+ using System.ComponentModel.Design;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Runtime.Remoting;
+ using System.Runtime.Remoting.Channels;
+ using System.Runtime.Remoting.Channels.Ipc;
+ using System.Runtime.Serialization.Formatters;
+ using Microsoft.VisualStudio.Shell;
+
+ internal sealed class IntegrationTestServiceCommands : IDisposable
+ {
+ public const int CmdIdStartIntegrationTestService = 0x5201;
+ public const int CmdIdStopIntegrationTestService = 0x5204;
+
+ public static readonly Guid GuidIntegrationTestCmdSet = new Guid("F3505B05-AF1E-493A-A5A5-ECEB69C42714");
+
+ private static readonly BinaryServerFormatterSinkProvider DefaultSinkProvider = new BinaryServerFormatterSinkProvider()
+ {
+ TypeFilterLevel = TypeFilterLevel.Full,
+ };
+
+ private readonly Package _package;
+
+ private readonly MenuCommand _startMenuCmd;
+ private readonly MenuCommand _stopMenuCmd;
+
+ private IntegrationService _service;
+ private IpcChannel _serviceChannel;
+ private ObjRef _marshalledService;
+
+ private IntegrationTestServiceCommands(Package package)
+ {
+ _package = package ?? throw new ArgumentNullException(nameof(package));
+
+ if (ServiceProvider.GetService(typeof(IMenuCommandService)) is OleMenuCommandService menuCommandService)
+ {
+ var startMenuCmdId = new CommandID(GuidIntegrationTestCmdSet, CmdIdStartIntegrationTestService);
+ _startMenuCmd = new MenuCommand(StartServiceCallback, startMenuCmdId)
+ {
+ Enabled = true,
+ Visible = true,
+ };
+ menuCommandService.AddCommand(_startMenuCmd);
+
+ var stopMenuCmdId = new CommandID(GuidIntegrationTestCmdSet, CmdIdStopIntegrationTestService);
+ _stopMenuCmd = new MenuCommand(StopServiceCallback, stopMenuCmdId)
+ {
+ Enabled = false,
+ Visible = false,
+ };
+ menuCommandService.AddCommand(_stopMenuCmd);
+ }
+ }
+
+ public static IntegrationTestServiceCommands Instance
+ {
+ get; private set;
+ }
+
+ private IServiceProvider ServiceProvider => _package;
+
+ public static void Initialize(Package package)
+ {
+ Instance = new IntegrationTestServiceCommands(package);
+ }
+
+ public void Dispose()
+ => StopServiceCallback(this, EventArgs.Empty);
+
+ ///
+ /// Starts the IPC server for the Integration Test service.
+ ///
+ private void StartServiceCallback(object sender, EventArgs e)
+ {
+ if (_startMenuCmd.Enabled)
+ {
+ _service = new IntegrationService();
+
+ _serviceChannel = new IpcChannel(
+ new Hashtable
+ {
+ { "name", $"Microsoft.VisualStudio.IntegrationTest.ServiceChannel_{Process.GetCurrentProcess().Id}" },
+ { "portName", _service.PortName },
+ },
+ clientSinkProvider: new BinaryClientFormatterSinkProvider(),
+ serverSinkProvider: DefaultSinkProvider);
+
+ var serviceType = typeof(IntegrationService);
+ _marshalledService = RemotingServices.Marshal(_service, serviceType.FullName, serviceType);
+
+ _serviceChannel.StartListening(null);
+ ChannelServices.RegisterChannel(_serviceChannel, ensureSecurity: true);
+
+ SwapAvailableCommands(_startMenuCmd, _stopMenuCmd);
+ }
+ }
+
+ /// Stops the IPC server for the Integration Test service.
+ private void StopServiceCallback(object sender, EventArgs e)
+ {
+ if (_stopMenuCmd.Enabled)
+ {
+ if (_serviceChannel != null)
+ {
+ if (ChannelServices.RegisteredChannels.Contains(_serviceChannel))
+ {
+ ChannelServices.UnregisterChannel(_serviceChannel);
+ }
+
+ _serviceChannel.StopListening(null);
+ _serviceChannel = null;
+ }
+
+ _marshalledService = null;
+ _service = null;
+
+ SwapAvailableCommands(_stopMenuCmd, _startMenuCmd);
+ }
+ }
+
+ private void SwapAvailableCommands(MenuCommand commandToDisable, MenuCommand commandToEnable)
+ {
+ commandToDisable.Enabled = false;
+ commandToDisable.Visible = false;
+
+ commandToEnable.Enabled = true;
+ commandToEnable.Visible = true;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServiceCommands.vsct b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServiceCommands.vsct
new file mode 100644
index 0000000..fd00f1f
--- /dev/null
+++ b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServiceCommands.vsct
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServicePackage.cs b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServicePackage.cs
new file mode 100644
index 0000000..cb96622
--- /dev/null
+++ b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServicePackage.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Microsoft.VisualStudio.IntegrationTestService
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using Microsoft.VisualStudio.Shell;
+
+ [Guid("78D5A8B5-1634-434B-802D-E3E4A46B1AA6")]
+ [PackageRegistration(UseManagedResourcesOnly = true)]
+ [ProvideMenuResource("Menus.ctmenu", version: 1)]
+ public sealed class IntegrationTestServicePackage : Package
+ {
+ protected override void Initialize()
+ {
+ base.Initialize();
+ IntegrationTestServiceCommands.Initialize(this);
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServicePackage.resx b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServicePackage.resx
new file mode 100644
index 0000000..4fdb1b6
--- /dev/null
+++ b/src/Microsoft.VisualStudio.IntegrationTestService/IntegrationTestServicePackage.resx
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.IntegrationTestService/Microsoft.VisualStudio.IntegrationTestService.csproj b/src/Microsoft.VisualStudio.IntegrationTestService/Microsoft.VisualStudio.IntegrationTestService.csproj
new file mode 100644
index 0000000..5680b90
--- /dev/null
+++ b/src/Microsoft.VisualStudio.IntegrationTestService/Microsoft.VisualStudio.IntegrationTestService.csproj
@@ -0,0 +1,53 @@
+
+
+
+
+
+ net45
+ Integration test service extension for Visual Studio
+
+
+
+ true
+ true
+ true
+ false
+ false
+ true
+ true
+
+
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LICENSE
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.IntegrationTestService/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.IntegrationTestService/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..4c72974
--- /dev/null
+++ b/src/Microsoft.VisualStudio.IntegrationTestService/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: CLSCompliant(false)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
diff --git a/src/Microsoft.VisualStudio.IntegrationTestService/source.extension.vsixmanifest b/src/Microsoft.VisualStudio.IntegrationTestService/source.extension.vsixmanifest
new file mode 100644
index 0000000..b8a9257
--- /dev/null
+++ b/src/Microsoft.VisualStudio.IntegrationTestService/source.extension.vsixmanifest
@@ -0,0 +1,27 @@
+
+
+
+
+ Visual Studio Integration Test Service
+ Integration test service.
+ https://github.com/Microsoft/vs-extension-testing
+ LICENSE
+
+
+ https://github.com/Microsoft/vs-extension-testing/releases/tag/1.0.0
+
+
+ vssdk, testing
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.11/Microsoft.VisualStudio.VsixInstaller.11.csproj b/src/Microsoft.VisualStudio.VsixInstaller.11/Microsoft.VisualStudio.VsixInstaller.11.csproj
new file mode 100644
index 0000000..ba8e52b
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.11/Microsoft.VisualStudio.VsixInstaller.11.csproj
@@ -0,0 +1,26 @@
+
+
+
+
+ net452
+ x86
+
+ $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\11.0@InstallDir)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.11/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.VsixInstaller.11/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..4c72974
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.11/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: CLSCompliant(false)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.12/Microsoft.VisualStudio.VsixInstaller.12.csproj b/src/Microsoft.VisualStudio.VsixInstaller.12/Microsoft.VisualStudio.VsixInstaller.12.csproj
new file mode 100644
index 0000000..7d64e1e
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.12/Microsoft.VisualStudio.VsixInstaller.12.csproj
@@ -0,0 +1,26 @@
+
+
+
+
+ net452
+ x86
+
+ $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\12.0@InstallDir)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.12/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.VsixInstaller.12/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..4c72974
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.12/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: CLSCompliant(false)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.14/Microsoft.VisualStudio.VsixInstaller.14.csproj b/src/Microsoft.VisualStudio.VsixInstaller.14/Microsoft.VisualStudio.VsixInstaller.14.csproj
new file mode 100644
index 0000000..45f6adf
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.14/Microsoft.VisualStudio.VsixInstaller.14.csproj
@@ -0,0 +1,26 @@
+
+
+
+
+ net452
+ x86
+
+ $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0@InstallDir)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.14/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.VsixInstaller.14/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..4c72974
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.14/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: CLSCompliant(false)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.15/Microsoft.VisualStudio.VsixInstaller.15.csproj b/src/Microsoft.VisualStudio.VsixInstaller.15/Microsoft.VisualStudio.VsixInstaller.15.csproj
new file mode 100644
index 0000000..717c54b
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.15/Microsoft.VisualStudio.VsixInstaller.15.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+ net46
+ x86
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.15/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.VsixInstaller.15/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..4c72974
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.15/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: CLSCompliant(false)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.Interop/IVsExtensionManagerPrivate.cs b/src/Microsoft.VisualStudio.VsixInstaller.Interop/IVsExtensionManagerPrivate.cs
new file mode 100644
index 0000000..6cc351f
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.Interop/IVsExtensionManagerPrivate.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Microsoft.Internal.VisualStudio.Shell.Interop
+{
+ using System;
+ using System.Runtime.CompilerServices;
+ using System.Runtime.InteropServices;
+
+ [ComImport]
+ [Guid("753E55C6-E779-4A7A-BCD1-FD87181D52C0")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ public interface IVsExtensionManagerPrivate
+ {
+ [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
+ int GetEnabledExtensionContentLocations(
+ [In] [MarshalAs(UnmanagedType.LPWStr)] string szContentTypeName,
+ [In] uint cContentLocations,
+ [Out] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr, SizeParamIndex = 1)] string[] rgbstrContentLocations,
+ [Out] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr, SizeParamIndex = 1)] string[] rgbstrUniqueExtensionStrings,
+ out uint pcContentLocations);
+
+ [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
+ int GetEnabledExtensionContentLocationsWithNames(
+ [In] [MarshalAs(UnmanagedType.LPWStr)] string szContentTypeName,
+ [In] uint cContentLocations,
+ [Out] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr, SizeParamIndex = 1)] string[] rgbstrContentLocations,
+ [Out] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr, SizeParamIndex = 1)] string[] rgbstrUniqueExtensionStrings,
+ [Out] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr, SizeParamIndex = 1)] string[] rgbstrExtensionNames,
+ out uint pcContentLocations);
+
+ [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
+ int GetDisabledExtensionContentLocations(
+ [In] [MarshalAs(UnmanagedType.LPWStr)] string szContentTypeName,
+ [In] uint cContentLocations,
+ [Out] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr, SizeParamIndex = 1)] string[] rgbstrContentLocations,
+ [Out] [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr, SizeParamIndex = 1)] string[] rgbstrUniqueExtensionStrings,
+ out uint pcContentLocations);
+
+ [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
+ int GetLastConfigurationChange([Out] [MarshalAs(UnmanagedType.LPArray)] DateTime[] pTimestamp);
+
+ [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
+ int LogAllInstalledExtensions();
+
+ [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
+ int GetUniqueExtensionString(
+ [In] [MarshalAs(UnmanagedType.LPWStr)] string szExtensionIdentifier,
+ [MarshalAs(UnmanagedType.BStr)] out string pbstrUniqueString);
+ }
+}
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.Interop/IVsExtensionManagerPrivate2.cs b/src/Microsoft.VisualStudio.VsixInstaller.Interop/IVsExtensionManagerPrivate2.cs
new file mode 100644
index 0000000..5acfc8b
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.Interop/IVsExtensionManagerPrivate2.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Microsoft.Internal.VisualStudio.Shell.Interop
+{
+ using System;
+ using System.Runtime.CompilerServices;
+ using System.Runtime.InteropServices;
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("6B741746-E3C9-434A-9E20-6E330D88C7F6")]
+ public interface IVsExtensionManagerPrivate2
+ {
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void GetAssetProperties(
+ [In] [MarshalAs(UnmanagedType.LPWStr)] string szAssetTypeName,
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaNames,
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaVersions,
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaAuthors,
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaExtensionIDs);
+
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void GetExtensionProperties(
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaNames,
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaVersions,
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaAuthors,
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaContentLocations,
+ [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out Array prgsaExtensionIDs);
+
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ ulong GetLastWriteTime([In] [MarshalAs(UnmanagedType.LPWStr)] string szContentTypeName);
+ }
+}
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.Interop/Microsoft.VisualStudio.VsixInstaller.Interop.csproj b/src/Microsoft.VisualStudio.VsixInstaller.Interop/Microsoft.VisualStudio.VsixInstaller.Interop.csproj
new file mode 100644
index 0000000..52e5335
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.Interop/Microsoft.VisualStudio.VsixInstaller.Interop.csproj
@@ -0,0 +1,8 @@
+
+
+
+
+ net45
+
+
+
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.Interop/Properties/AssemblyInfo.cs b/src/Microsoft.VisualStudio.VsixInstaller.Interop/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..66432df
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.Interop/Properties/AssemblyInfo.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+using System;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: CLSCompliant(false)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+[assembly: PrimaryInteropAssembly(12, 0)]
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.Shared/Installer.cs b/src/Microsoft.VisualStudio.VsixInstaller.Shared/Installer.cs
new file mode 100644
index 0000000..859b0b9
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.Shared/Installer.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for more information.
+
+namespace Microsoft.VisualStudio.VsixInstaller
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Reflection;
+ using System.Runtime.CompilerServices;
+ using Microsoft.VisualStudio.ExtensionManager;
+ using Microsoft.VisualStudio.Settings;
+ using File = System.IO.File;
+ using Path = System.IO.Path;
+
+ public static class Installer
+ {
+ public static void Install(IEnumerable vsixFiles, string installationPath, string rootSuffix)
+ {
+ AppDomain.CurrentDomain.AssemblyResolve += HandleAssemblyResolve;
+
+ try
+ {
+ InstallImpl(vsixFiles, rootSuffix, installationPath);
+ }
+ finally
+ {
+ AppDomain.CurrentDomain.AssemblyResolve -= HandleAssemblyResolve;
+ }
+
+ return;
+
+ Assembly HandleAssemblyResolve(object sender, ResolveEventArgs args)
+ {
+ string path = Path.Combine(installationPath, @"Common7\IDE\PrivateAssemblies", new AssemblyName(args.Name).Name + ".dll");
+ if (File.Exists(path))
+ {
+ return Assembly.LoadFrom(path);
+ }
+
+ path = Path.Combine(installationPath, @"Common7\IDE", new AssemblyName(args.Name).Name + ".dll");
+ if (File.Exists(path))
+ {
+ return Assembly.LoadFrom(path);
+ }
+
+ path = Path.Combine(installationPath, @"Common7\IDE\PublicAssemblies", new AssemblyName(args.Name).Name + ".dll");
+ if (File.Exists(path))
+ {
+ return Assembly.LoadFrom(path);
+ }
+
+ return null;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void InstallImpl(IEnumerable vsixFiles, string rootSuffix, string installationPath)
+ {
+ var vsExeFile = Path.Combine(installationPath, @"Common7\IDE\devenv.exe");
+
+ using (var settingsManager = ExternalSettingsManager.CreateForApplication(vsExeFile, rootSuffix))
+ {
+ var extensionManager = new ExtensionManagerService(settingsManager);
+ IVsExtensionManager vsExtensionManager = extensionManager;
+ var extensions = vsixFiles.Select(vsExtensionManager.CreateInstallableExtension).ToArray();
+
+ foreach (var extension in extensions)
+ {
+ if (extensionManager.IsInstalled(extension))
+ {
+ extensionManager.Uninstall(extensionManager.GetInstalledExtension(extension.Header.Identifier));
+ }
+ }
+
+ foreach (var extension in extensions)
+ {
+ extensionManager.Install(extension, perMachine: false);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.Shared/Microsoft.VisualStudio.VsixInstaller.Shared.projitems b/src/Microsoft.VisualStudio.VsixInstaller.Shared/Microsoft.VisualStudio.VsixInstaller.Shared.projitems
new file mode 100644
index 0000000..fa4c0a6
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.Shared/Microsoft.VisualStudio.VsixInstaller.Shared.projitems
@@ -0,0 +1,14 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ b3bed6cb-abfe-4bb8-8af7-901ff0c6f027
+
+
+ Microsoft.VisualStudio.VsixInstaller.Shared
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.VsixInstaller.Shared/Microsoft.VisualStudio.VsixInstaller.Shared.shproj b/src/Microsoft.VisualStudio.VsixInstaller.Shared/Microsoft.VisualStudio.VsixInstaller.Shared.shproj
new file mode 100644
index 0000000..9d9106b
--- /dev/null
+++ b/src/Microsoft.VisualStudio.VsixInstaller.Shared/Microsoft.VisualStudio.VsixInstaller.Shared.shproj
@@ -0,0 +1,13 @@
+
+
+
+ b3bed6cb-abfe-4bb8-8af7-901ff0c6f027
+ 14.0
+
+
+
+
+
+
+
+
diff --git a/src/stylecop.json b/src/stylecop.json
new file mode 100644
index 0000000..50134f7
--- /dev/null
+++ b/src/stylecop.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "documentationRules": {
+ "companyName": "Microsoft",
+ "xmlHeader": false,
+ "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName}. See {licenseFile} in the project root for more information.",
+ "fileNamingConvention": "metadata",
+ "variables": {
+ "licenseName": "MIT License",
+ "licenseFile": "LICENSE"
+ }
+ },
+ "layoutRules": {
+ "allowConsecutiveUsings": true,
+ "newlineAtEndOfFile": "require"
+ }
+ }
+}