Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port BasicNavigationBar.CodeSpit to the new test harness #57738

Merged
merged 5 commits into from
Nov 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ protected CancellationToken HangMitigatingCancellationToken
public virtual async Task InitializeAsync()
{
TestServices = await CreateTestServicesAsync();

await TestServices.StateReset.ResetGlobalOptionsAsync(HangMitigatingCancellationToken);
await TestServices.StateReset.ResetHostSettingsAsync(HangMitigatingCancellationToken);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,38 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Implementation.Suggestions;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.UnitTests;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.IntegrationTest.Utilities;
using Microsoft.VisualStudio.IntegrationTest.Utilities.Input;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
using Xunit;
using IObjectWithSite = Microsoft.VisualStudio.OLE.Interop.IObjectWithSite;
using IOleCommandTarget = Microsoft.VisualStudio.OLE.Interop.IOleCommandTarget;
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
using OLECMDEXECOPT = Microsoft.VisualStudio.OLE.Interop.OLECMDEXECOPT;

namespace Roslyn.VisualStudio.IntegrationTests.InProcess
Expand Down Expand Up @@ -132,6 +144,186 @@ public async Task SetUseSuggestionModeAsync(bool value, CancellationToken cancel
}
}

#region Navigation bars

public async Task ExpandNavigationBarAsync(NavigationBarDropdownKind index, CancellationToken cancellationToken)
{
await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);

await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var view = await GetActiveTextViewAsync(cancellationToken);
var combobox = (await GetNavigationBarComboBoxesAsync(view, cancellationToken))[(int)index];
FocusManager.SetFocusedElement(FocusManager.GetFocusScope(combobox), combobox);
combobox.IsDropDownOpen = true;
}

public async Task<ImmutableArray<string>> GetNavigationBarItemsAsync(NavigationBarDropdownKind index, CancellationToken cancellationToken)
{
await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);

await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var view = await GetActiveTextViewAsync(cancellationToken);
var combobox = (await GetNavigationBarComboBoxesAsync(view, cancellationToken))[(int)index];
return combobox.Items.OfType<object>().SelectAsArray(i => $"{i}");
sharwell marked this conversation as resolved.
Show resolved Hide resolved
}

public async Task<string?> GetNavigationBarSelectionAsync(NavigationBarDropdownKind index, CancellationToken cancellationToken)
{
await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);

await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var view = await GetActiveTextViewAsync(cancellationToken);
var combobox = (await GetNavigationBarComboBoxesAsync(view, cancellationToken))[(int)index];
return combobox.SelectedItem?.ToString();
}

public async Task SelectNavigationBarItemAsync(NavigationBarDropdownKind index, string item, CancellationToken cancellationToken)
{
await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);

await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var itemIndex = await GetNavigationBarItemIndexAsync(index, item, cancellationToken);
if (itemIndex < 0)
{
Assert.Contains(item, await GetNavigationBarItemsAsync(index, cancellationToken));
throw ExceptionUtilities.Unreachable;
}

await ExpandNavigationBarAsync(index, cancellationToken);
await TestServices.Input.SendAsync(VirtualKey.Home);
for (var i = 0; i < itemIndex; i++)
{
await TestServices.Input.SendAsync(VirtualKey.Down);
}
sharwell marked this conversation as resolved.
Show resolved Hide resolved

await TestServices.Input.SendAsync(VirtualKey.Enter);

// Navigation and/or code generation following selection is tracked under FeatureAttribute.NavigationBar
await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.NavigationBar, cancellationToken);
}

public async Task<int> GetNavigationBarItemIndexAsync(NavigationBarDropdownKind index, string item, CancellationToken cancellationToken)
{
var items = await GetNavigationBarItemsAsync(index, cancellationToken);
return items.IndexOf(item);
}

public async Task<bool> IsNavigationBarEnabledAsync(CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var view = await GetActiveTextViewAsync(cancellationToken);
return (await GetNavigationBarMarginAsync(view, cancellationToken)) is not null;
}

private async Task<List<ComboBox>> GetNavigationBarComboBoxesAsync(IWpfTextView textView, CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var margin = await GetNavigationBarMarginAsync(textView, cancellationToken);
return margin.GetFieldValue<List<ComboBox>>("_combos");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well that's terrifying :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Copied from old test harness. Happy to use a new approach if we find one.

}

private async Task<UIElement?> GetNavigationBarMarginAsync(IWpfTextView textView, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good god.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

who do we send feedback to that VS/editor needs a proper, resilient, testing API so we can do this sort of validation without this stuff?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe @AmadeusW ?

{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

var editorAdaptersFactoryService = await GetComponentModelServiceAsync<IVsEditorAdaptersFactoryService>(cancellationToken);
var viewAdapter = editorAdaptersFactoryService.GetViewAdapter(textView);
Assumes.Present(viewAdapter);

// Make sure we have the top pane
//
// The docs are wrong. When a secondary view exists, it is the secondary view which is on top. The primary
// view is only on top when there is no secondary view.
var codeWindow = TryGetCodeWindow(viewAdapter);
Assumes.Present(codeWindow);

if (ErrorHandler.Succeeded(codeWindow.GetSecondaryView(out var secondaryViewAdapter)))
{
viewAdapter = secondaryViewAdapter;
}

var textViewHost = editorAdaptersFactoryService.GetWpfTextViewHost(viewAdapter);
Assumes.Present(textViewHost);

var dropDownMargin = textViewHost.GetTextViewMargin("DropDownMargin");
if (dropDownMargin != null)
{
return ((Decorator)dropDownMargin.VisualElement).Child;
}

return null;

static IVsCodeWindow? TryGetCodeWindow(IVsTextView textView)
{
if (textView is not IObjectWithSite objectWithSite)
{
return null;
}

var riid = typeof(IOleServiceProvider).GUID;
objectWithSite.GetSite(ref riid, out var ppvSite);
if (ppvSite == IntPtr.Zero)
{
return null;
}

IOleServiceProvider? oleServiceProvider = null;
try
{
oleServiceProvider = Marshal.GetObjectForIUnknown(ppvSite) as IOleServiceProvider;
}
finally
{
Marshal.Release(ppvSite);
}

if (oleServiceProvider == null)
{
return null;
}

var guidService = typeof(SVsWindowFrame).GUID;
riid = typeof(IVsWindowFrame).GUID;
if (ErrorHandler.Failed(oleServiceProvider.QueryService(ref guidService, ref riid, out var ppvObject)) || ppvObject == IntPtr.Zero)
{
return null;
}

IVsWindowFrame? frame = null;
try
{
frame = (IVsWindowFrame)Marshal.GetObjectForIUnknown(ppvObject);
}
finally
{
Marshal.Release(ppvObject);
}

riid = typeof(IVsCodeWindow).GUID;
if (ErrorHandler.Failed(frame.QueryViewInterface(ref riid, out ppvObject)) || ppvObject == IntPtr.Zero)
{
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be useful at all to have logs on failure cases here, or do the dumps typically contain all the information we'd need to verify which of these error cases we're hitting?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ It definitely wouldn't hurt, especially considering the next like after calling TryGetCodeWindow is always Assumes.Present. For now I'm OK keeping this form for two reasons:

  1. This code historically has not failed (copied from older harness)
  2. In the event of failure, we'll get a message saying IVsCodeWindow was expected but not available, and at that point it would be easy to update the implementation to better explain the point of failure

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the event of failure, we'll get a message saying IVsCodeWindow was expected but not available, and at that point it would be easy to update the implementation to better explain the point of failure

taht wfm. it's generally waht i've done when there are issues and the logs aren't sufficient.

}

try
{
return Marshal.GetObjectForIUnknown(ppvObject) as IVsCodeWindow;
}
finally
{
Marshal.Release(ppvObject);
}
}
}

#endregion

public async Task DismissLightBulbSessionAsync(CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
Expand Down Expand Up @@ -176,6 +368,17 @@ public async Task InvokeCodeActionListAsync(CancellationToken cancellationToken)
await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.SolutionCrawler, cancellationToken);
await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.DiagnosticService, cancellationToken);

if (Version.Parse("17.1.31916.450") > await TestServices.Shell.GetVersionAsync(cancellationToken))
{
// Workaround for extremely unstable async lightbulb prior to:
// https://devdiv.visualstudio.com/DevDiv/_git/VS-Platform/pullrequest/361759
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we track removing this somehow?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will disable itself at the appropriate time when integration test machines update, so I probably wouldn't bother tracking separately. If we file an issue, it will likely just end up in the backlog and never get seen again, where it may or may not be updated after someone eventually removes this block.

I'd probably take a different approach if this was production code.

await TestServices.Input.SendAsync(new KeyPress(VirtualKey.Period, ShiftState.Ctrl));
await Task.Delay(5000, cancellationToken);

await TestServices.Editor.DismissLightBulbSessionAsync(cancellationToken);
await Task.Delay(5000, cancellationToken);
}

await ShowLightBulbAsync(cancellationToken);
await TestServices.Workspace.WaitForAsyncOperationsAsync(FeatureAttribute.LightBulb, cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.IntegrationTest.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Roslyn.VisualStudio.IntegrationTests.InProcess
{
Expand All @@ -22,6 +23,94 @@ public EditorVerifierInProcess(TestServices testServices)
{
}

public async Task CurrentLineTextAsync(
string expectedText,
bool assertCaretPosition = false,
CancellationToken cancellationToken = default)
{
if (assertCaretPosition)
{
await CurrentLineTextAndAssertCaretPositionAsync(expectedText, cancellationToken);
}
else
{
var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
var lineText = view.Caret.Position.BufferPosition.GetContainingLine().GetText();
Assert.Equal(expectedText, lineText);
}
}

private async Task CurrentLineTextAndAssertCaretPositionAsync(
string expectedText,
CancellationToken cancellationToken)
{
var expectedCaretIndex = expectedText.IndexOf("$$");
if (expectedCaretIndex < 0)
{
throw new ArgumentException("Expected caret position to be specified with $$", nameof(expectedText));
}

var expectedCaretMarkupEndIndex = expectedCaretIndex + "$$".Length;

var expectedTextBeforeCaret = expectedText.Substring(0, expectedCaretIndex);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - maybe have a small helper to extract the text w/out the $$ since it looks like you do it here and on line 129

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ It looks like this is cleaned up a bit in a pull request I'm submitting after this one, so I'll leave it here for now.

var expectedTextAfterCaret = expectedText.Substring(expectedCaretMarkupEndIndex);

var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
var bufferPosition = view.Caret.Position.BufferPosition;
var line = bufferPosition.GetContainingLine();
var lineText = line.GetText();
var lineTextBeforeCaret = lineText[..(bufferPosition.Position - line.Start)];
var lineTextAfterCaret = lineText[(bufferPosition.Position - line.Start)..];

Assert.Equal(expectedTextBeforeCaret, lineTextBeforeCaret);
Assert.Equal(expectedTextAfterCaret, lineTextAfterCaret);
Assert.Equal(expectedTextBeforeCaret.Length + expectedTextAfterCaret.Length, lineText.Length);
}

public async Task TextContainsAsync(
string expectedText,
bool assertCaretPosition = false,
CancellationToken cancellationToken = default)
{
if (assertCaretPosition)
{
await TextContainsAndAssertCaretPositionAsync(expectedText, cancellationToken);
}
else
{
var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
var editorText = view.TextSnapshot.GetText();
Assert.Contains(expectedText, editorText);
}
}

private async Task TextContainsAndAssertCaretPositionAsync(
string expectedText,
CancellationToken cancellationToken)
{
var caretStartIndex = expectedText.IndexOf("$$");
if (caretStartIndex < 0)
{
throw new ArgumentException("Expected caret position to be specified with $$", nameof(expectedText));
}

var caretEndIndex = caretStartIndex + "$$".Length;

var expectedTextBeforeCaret = expectedText[..caretStartIndex];
var expectedTextAfterCaret = expectedText[caretEndIndex..];

var expectedTextWithoutCaret = expectedTextBeforeCaret + expectedTextAfterCaret;

var view = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
var editorText = view.TextSnapshot.GetText();
Assert.Contains(expectedTextWithoutCaret, editorText);

var index = editorText.IndexOf(expectedTextWithoutCaret);

var caretPosition = await TestServices.Editor.GetCaretPositionAsync(cancellationToken);
Assert.Equal(caretStartIndex + index, caretPosition);
}

public async Task CodeActionAsync(
string expectedItem,
bool applyFix = false,
Expand Down Expand Up @@ -113,5 +202,10 @@ public async Task CodeActionsNotShowingAsync(CancellationToken cancellationToken
throw new InvalidOperationException("Expected no light bulb session, but one was found.");
}
}

public async Task CaretPositionAsync(int expectedCaretPosition, CancellationToken cancellationToken)
{
Assert.Equal(expectedCaretPosition, await TestServices.Editor.GetCaretPositionAsync(cancellationToken));
}
}
}
Loading