|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Threading;
using System.Threading.Tasks;
using EnvDTE;
using Microsoft.VisualStudio.IntegrationTest.Utilities.InProcess;
using Microsoft.VisualStudio.IntegrationTest.Utilities.Input;
using Microsoft.VisualStudio.IntegrationTest.Utilities.OutOfProcess;
using Xunit;
using Process = System.Diagnostics.Process;
namespace Microsoft.VisualStudio.IntegrationTest.Utilities
{
public class VisualStudioInstance
{
/// <summary>
/// Used for creating unique IPC channel names each time a Visual Studio instance is created during tests.
/// </summary>
/// <seealso cref="GetIpcClientChannelName"/>
private static int s_connectionIndex = 0;
private readonly IntegrationService _integrationService;
private readonly IpcClientChannel _integrationServiceChannel;
private readonly VisualStudio_InProc _inProc;
public AddParameterDialog_OutOfProc AddParameterDialog { get; }
public ChangeSignatureDialog_OutOfProc ChangeSignatureDialog { get; }
public CodeDefinitionWindow_OutOfProc CodeDefinitionWindow { get; }
public CSharpInteractiveWindow_OutOfProc InteractiveWindow { get; }
public ObjectBrowserWindow_OutOfProc ObjectBrowserWindow { get; }
public Debugger_OutOfProc Debugger { get; }
public Editor_OutOfProc Editor { get; }
public ErrorList_OutOfProc ErrorList { get; }
public ExtractInterfaceDialog_OutOfProc ExtractInterfaceDialog { get; }
public GenerateTypeDialog_OutOfProc GenerateTypeDialog { get; }
public ImmediateWindow_OutOfProc ImmediateWindow { get; }
public LocalsWindow_OutOfProc LocalsWindow { get; set; }
public MoveToNamespaceDialog_OutOfProc MoveToNamespaceDialog { get; }
public PickMembersDialog_OutOfProc PickMembersDialog { get; set; }
public SendKeys SendKeys { get; }
public Shell_OutOfProc Shell { get; }
public SolutionExplorer_OutOfProc SolutionExplorer { get; }
public VisualStudioWorkspace_OutOfProc Workspace { get; }
public StartPage_OutOfProc StartPage { get; }
internal DTE Dte { get; }
internal Process HostProcess { get; }
/// <summary>
/// The set of Visual Studio packages that are installed into this instance.
/// </summary>
public ImmutableHashSet<string> SupportedPackageIds { get; }
/// <summary>
/// The path to the root of this installed version of Visual Studio. This is the folder that contains
/// Common7\IDE.
/// </summary>
public string InstallationPath { get; }
public bool IsUsingLspEditor { get; }
public VisualStudioInstance(Process hostProcess, DTE dte, ImmutableHashSet<string> supportedPackageIds, string installationPath, bool isUsingLspEditor)
{
HostProcess = hostProcess;
Dte = dte;
SupportedPackageIds = supportedPackageIds;
InstallationPath = installationPath;
IsUsingLspEditor = isUsingLspEditor;
if (System.Diagnostics.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();
var targetProcessId = Process.GetCurrentProcess().Id;
var localProcess = debuggerHostDte?.Debugger.LocalProcesses.OfType<EnvDTE80.Process2>().FirstOrDefault(p => p.ProcessID == hostProcess.Id);
localProcess?.Attach2("Managed");
}
StartRemoteIntegrationService(dte);
_integrationServiceChannel = new IpcClientChannel(GetIpcClientChannelName(HostProcess), sinkProvider: null);
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<VisualStudio_InProc>(
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();
AddParameterDialog = new AddParameterDialog_OutOfProc(this);
ChangeSignatureDialog = new ChangeSignatureDialog_OutOfProc(this);
CodeDefinitionWindow = new CodeDefinitionWindow_OutOfProc(this);
InteractiveWindow = new CSharpInteractiveWindow_OutOfProc(this);
ObjectBrowserWindow = new ObjectBrowserWindow_OutOfProc(this);
Debugger = new Debugger_OutOfProc(this);
Editor = new Editor_OutOfProc(this);
ErrorList = new ErrorList_OutOfProc(this);
ExtractInterfaceDialog = new ExtractInterfaceDialog_OutOfProc(this);
GenerateTypeDialog = new GenerateTypeDialog_OutOfProc(this);
ImmediateWindow = new ImmediateWindow_OutOfProc(this);
LocalsWindow = new LocalsWindow_OutOfProc(this);
MoveToNamespaceDialog = new MoveToNamespaceDialog_OutOfProc(this);
PickMembersDialog = new PickMembersDialog_OutOfProc(this);
Shell = new Shell_OutOfProc(this);
SolutionExplorer = new SolutionExplorer_OutOfProc(this);
Workspace = new VisualStudioWorkspace_OutOfProc(this);
StartPage = new StartPage_OutOfProc(this);
SendKeys = new SendKeys(this);
// Ensure we are in a known 'good' state by cleaning up anything changed by the previous instance
CleanUp();
}
private static string GetIpcClientChannelName(Process hostProcess)
{
var index = Interlocked.Increment(ref s_connectionIndex) - 1;
if (index == 0)
{
return $"IPC channel client for {hostProcess.Id}";
}
else
{
return $"IPC channel client for {hostProcess.Id} ({index})";
}
}
public void ExecuteInHostProcess(Type type, string methodName)
{
var result = _integrationService.Execute(type.Assembly.Location, type.FullName, methodName);
if (result != null)
{
throw new InvalidOperationException("The specified call was not expected to return a value.");
}
}
public T ExecuteInHostProcess<T>(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 ActivateMainWindow()
=> _inProc.ActivateMainWindow();
public void WaitForApplicationIdle(CancellationToken cancellationToken)
{
var task = Task.Factory.StartNew(() => _inProc.WaitForApplicationIdle(Helper.HangMitigatingTimeout), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
task.Wait(cancellationToken);
}
public void ExecuteCommand(string commandName, string argument = "")
=> _inProc.ExecuteCommand(commandName, argument);
public bool IsCommandAvailable(string commandName)
=> _inProc.IsCommandAvailable(commandName);
public string[] GetAvailableCommands()
=> _inProc.GetAvailableCommands();
public int ErrorListErrorCount
=> _inProc.GetErrorListErrorCount();
public void WaitForNoErrorsInErrorList()
=> _inProc.WaitForNoErrorsInErrorList();
public bool IsRunning => !HostProcess.HasExited;
public void CleanUp()
{
Workspace.CleanUpWaitingService();
Workspace.CleanUpWorkspace();
SolutionExplorer.CleanUpOpenSolution();
Workspace.WaitForAllAsyncOperationsOrFail(Helper.HangMitigatingTimeout);
// Close any windows leftover from previous (failed) tests
InteractiveWindow.CloseInteractiveWindow();
ObjectBrowserWindow.CloseWindow();
ChangeSignatureDialog.CloseWindow();
GenerateTypeDialog.CloseWindow();
ExtractInterfaceDialog.CloseWindow();
MoveToNamespaceDialog.CloseWindow();
PickMembersDialog.CloseWindow();
StartPage.CloseWindow();
// Prevent the start page from showing after each solution closes
StartPage.SetEnabled(false);
Workspace.ResetOptions();
}
public void Close(bool exitHostProcess = true)
{
if (!IsRunning)
{
CloseRemotingService(allowInProcCalls: false);
return;
}
try
{
CleanUp();
}
catch
{
// A cleanup failure occurred, but we still need to close the communication channel from this side
CloseRemotingService(allowInProcCalls: false);
throw;
}
CloseRemotingService(allowInProcCalls: true);
if (exitHostProcess)
{
CloseHostProcess();
}
}
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<EnvDTE.Process>().Any(p => p.ProcessID == currentProcessId) ?? false)
{
return dte;
}
}
return null;
}
private void CloseHostProcess()
{
_inProc.Quit();
if (!HostProcess.WaitForExit(milliseconds: 10000))
{
IntegrationHelper.KillProcess(HostProcess);
}
}
private void CloseRemotingService(bool allowInProcCalls)
{
try
{
if (allowInProcCalls)
{
StopRemoteIntegrationService();
}
}
finally
{
if (_integrationServiceChannel != null
&& ChannelServices.RegisteredChannels.Contains(_integrationServiceChannel))
{
ChannelServices.UnregisterChannel(_integrationServiceChannel);
}
}
}
private static 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.Test_IntegrationTestService_Start).IsAvailable)
{
dte.ExecuteCommand(WellKnownCommandNames.Test_IntegrationTestService_Start);
}
}
private void StopRemoteIntegrationService()
{
if (_inProc.IsCommandAvailable(WellKnownCommandNames.Test_IntegrationTestService_Stop))
{
_inProc.ExecuteCommand(WellKnownCommandNames.Test_IntegrationTestService_Stop);
}
}
public TelemetryVerifier EnableTestTelemetryChannel()
{
_inProc.EnableTestTelemetryChannel();
return new TelemetryVerifier(this);
}
private void DisableTestTelemetryChannel()
=> _inProc.DisableTestTelemetryChannel();
/// <summary>
/// Waits for specific telemetry events to occur.
/// </summary>
/// <param name="names">The telemetry events to wait for.</param>
/// <returns><see langword="true"/> if the telemetry events occurred; otherwise, <see langword="false"/> if
/// telemetry is disabled.</returns>
private bool TryWaitForTelemetryEvents(string[] names)
=> _inProc.TryWaitForTelemetryEvents(names);
public class TelemetryVerifier : IDisposable
{
internal VisualStudioInstance _instance;
public TelemetryVerifier(VisualStudioInstance instance)
{
_instance = instance;
}
public void Dispose() => _instance.DisableTestTelemetryChannel();
/// <summary>
/// Asserts that a telemetry event of the given name was fired. Does not
/// do any additional validation (of performance numbers, etc).
/// </summary>
/// <param name="expectedEventNames"></param>
public void VerifyFired(params string[] expectedEventNames)
{
var telemetryEnabled = _instance.TryWaitForTelemetryEvents(expectedEventNames);
if (string.Equals(Environment.GetEnvironmentVariable("ROSLYN_TEST_CI"), "true", StringComparison.OrdinalIgnoreCase))
{
// Telemetry verification is optional for developer machines, but required for CI.
Assert.True(telemetryEnabled);
}
}
}
}
}
|