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" + } + } +}