/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/EditorFeatures/CSharpTest/NavigateTo/NavigateToSearcherTests.cs
557 строк
26 KB
Cyrus Najmabadi
Reapply "Update methods to be `async`." (#81808)
31 дек 2025, 01:59
31 дек 2025, 01:59
3d73546
Код
Авторство
О чём код?
// 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.Concurrent; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Test; using Microsoft.CodeAnalysis.Editor.UnitTests; using Microsoft.CodeAnalysis.NavigateTo; using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.PatternMatching; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Moq; using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.NavigateTo; [UseExportProvider] [Trait(Traits.Feature, Traits.Features.NavigateTo)] public sealed class NavigateToSearcherTests { private static readonly TestComposition FirstActiveAndVisibleComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(FirstDocumentIsActiveAndVisibleDocumentTrackingService.Factory)); private static void SetupSearchProject( Mock<IAdvancedNavigateToSearchService> searchService, string pattern, bool isFullyLoaded, ImmutableArray<INavigateToSearchResult> results) { if (isFullyLoaded) { // First do a full search searchService.Setup(ss => ss.SearchProjectsAsync( It.IsAny<Solution>(), It.IsAny<ImmutableArray<Project>>(), It.IsAny<ImmutableArray<Document>>(), pattern, ImmutableHashSet<string>.Empty, It.IsAny<Document?>(), It.IsAny<Func<ImmutableArray<INavigateToSearchResult>, Task>>(), It.IsAny<Func<Task>>(), It.IsAny<CancellationToken>())).Callback( (Solution solution, ImmutableArray<Project> projects, ImmutableArray<Document> priorityDocuments, string pattern, IImmutableSet<string> kinds, Document? activeDocument, Func<ImmutableArray<INavigateToSearchResult>, Task> onResultsFound, Func<Task> onProjectCompleted, CancellationToken cancellationToken) => { if (results.Length > 0) onResultsFound(results); }).Returns(Task.CompletedTask); searchService.Setup(ss => ss.SearchGeneratedDocumentsAsync( It.IsAny<Solution>(), It.IsAny<ImmutableArray<Project>>(), pattern, ImmutableHashSet<string>.Empty, It.IsAny<Document?>(), It.IsAny<Func<ImmutableArray<INavigateToSearchResult>, Task>>(), It.IsAny<Func<Task>>(), It.IsAny<CancellationToken>())).Callback( (Solution solution, ImmutableArray<Project> projects, string pattern, IImmutableSet<string> kinds, Document? activeDocument, Func<ImmutableArray<INavigateToSearchResult>, Task> onResultsFound, Func<Task> onProjectCompleted, CancellationToken cancellationToken) => { if (results.Length > 0) onResultsFound(results); }).Returns(Task.CompletedTask); // Followed by a generated doc search. } else { searchService.Setup(ss => ss.SearchCachedDocumentsAsync( It.IsAny<Solution>(), It.IsAny<ImmutableArray<Project>>(), It.IsAny<ImmutableArray<Document>>(), pattern, ImmutableHashSet<string>.Empty, It.IsAny<Document?>(), It.IsAny<Func<ImmutableArray<INavigateToSearchResult>, Task>>(), It.IsAny<Func<Task>>(), It.IsAny<CancellationToken>())).Callback( (Solution solution, ImmutableArray<Project> projects, ImmutableArray<Document> priorityDocuments, string pattern2, IImmutableSet<string> kinds, Document? activeDocument, Func<ImmutableArray<INavigateToSearchResult>, Task> onResultsFound2, Func<Task> onProjectCompleted, CancellationToken cancellationToken) => { if (results.Length > 0) onResultsFound2(results); }).Returns(Task.CompletedTask); } } private static async ValueTask<bool> IsFullyLoadedAsync(bool projectSystem, bool remoteHost) => projectSystem && remoteHost; [Fact] public async Task NotFullyLoadedOnlyMakesOneSearchProjectCallIfValueReturned() { using var workspace = EditorTestWorkspace.CreateCSharp(""); var pattern = "irrelevant"; var results = ImmutableArray.Create<INavigateToSearchResult>(new TestNavigateToSearchResult(workspace, new TextSpan(0, 0))); var searchService = new Mock<IAdvancedNavigateToSearchService>(MockBehavior.Strict); SetupSearchProject(searchService, pattern, isFullyLoaded: false, results); // Simulate a host that says the solution isn't fully loaded. var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict); hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(() => IsFullyLoadedAsync(projectSystem: false, remoteHost: false)); hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(searchService.Object); var callbackMock = new Mock<INavigateToSearchCallback>(MockBehavior.Strict); callbackMock.Setup(c => c.ReportIncomplete()); callbackMock.Setup(c => c.ReportProgress(It.IsAny<int>(), It.IsAny<int>())); callbackMock.Setup(c => c.AddResultsAsync(results, It.IsAny<Document>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask); // Because we returned a result when not fully loaded, we should notify the user that data was not complete. callbackMock.Setup(c => c.Done(false)); var searcher = NavigateToSearcher.Create( workspace.CurrentSolution, callbackMock.Object, pattern, kinds: ImmutableHashSet<string>.Empty, hostMock.Object); await searcher.SearchAsync(NavigateToSearchScope.Solution, CancellationToken.None); } [Theory, CombinatorialData] public async Task NotFullyLoadedMakesTwoSearchProjectCallIfValueNotReturned(bool projectSystemFullyLoaded) { using var workspace = EditorTestWorkspace.CreateCSharp(""); var pattern = "irrelevant"; var results = ImmutableArray.Create<INavigateToSearchResult>(new TestNavigateToSearchResult(workspace, new TextSpan(0, 0))); var searchService = new Mock<IAdvancedNavigateToSearchService>(MockBehavior.Strict); // First call will pass in that we're not fully loaded. If we return null, we should get // another call with the request to search the fully loaded data. SetupSearchProject(searchService, pattern, isFullyLoaded: false, results: []); SetupSearchProject(searchService, pattern, isFullyLoaded: true, results); // Simulate a host that says the solution isn't fully loaded. var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict); hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(() => IsFullyLoadedAsync(projectSystemFullyLoaded, remoteHost: false)); hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(searchService.Object); var callbackMock = new Mock<INavigateToSearchCallback>(MockBehavior.Strict); callbackMock.Setup(c => c.ReportIncomplete()); callbackMock.Setup(c => c.ReportProgress(It.IsAny<int>(), It.IsAny<int>())); callbackMock.Setup(c => c.AddResultsAsync(results, It.IsAny<Document>(), It.IsAny<CancellationToken>())) .Returns(Task.CompletedTask); // Because the remote host wasn't fully loaded, we still notify that our results may be incomplete. callbackMock.Setup(c => c.Done(false)); var searcher = NavigateToSearcher.Create( workspace.CurrentSolution, callbackMock.Object, pattern, kinds: ImmutableHashSet<string>.Empty, hostMock.Object); await searcher.SearchAsync(NavigateToSearchScope.Solution, CancellationToken.None); } [Theory, CombinatorialData] public async Task NotFullyLoadedStillReportsAsNotCompleteIfRemoteHostIsStillHydrating(bool projectIsFullyLoaded) { using var workspace = EditorTestWorkspace.CreateCSharp(""); var pattern = "irrelevant"; var searchService = new Mock<IAdvancedNavigateToSearchService>(MockBehavior.Strict); // First call will pass in that we're not fully loaded. If we return null, we should get another call with // the request to search the fully loaded data. If we don't report anything the second time, we will still // tell the user the search was complete. SetupSearchProject(searchService, pattern, isFullyLoaded: false, results: []); SetupSearchProject(searchService, pattern, isFullyLoaded: true, results: []); // Simulate a host that says the solution isn't fully loaded. var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict); hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(() => IsFullyLoadedAsync(projectIsFullyLoaded, remoteHost: false)); hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(searchService.Object); var callbackMock = new Mock<INavigateToSearchCallback>(MockBehavior.Strict); callbackMock.Setup(c => c.ReportIncomplete()); callbackMock.Setup(c => c.ReportProgress(It.IsAny<int>(), It.IsAny<int>())); // Because the remote host wasn't fully loaded, we still notify that our results may be incomplete. callbackMock.Setup(c => c.Done(false)); var searcher = NavigateToSearcher.Create( workspace.CurrentSolution, callbackMock.Object, pattern, kinds: ImmutableHashSet<string>.Empty, hostMock.Object); await searcher.SearchAsync(NavigateToSearchScope.Solution, CancellationToken.None); } [Fact] public async Task FullyLoadedMakesSingleSearchProjectCallIfValueNotReturned() { using var workspace = EditorTestWorkspace.CreateCSharp(""); var pattern = "irrelevant"; var results = ImmutableArray.Create<INavigateToSearchResult>(new TestNavigateToSearchResult(workspace, new TextSpan(0, 0))); var searchService = new Mock<IAdvancedNavigateToSearchService>(MockBehavior.Strict); // First call will pass in that we're fully loaded. If we return null, we should not get another call. SetupSearchProject(searchService, pattern, isFullyLoaded: true, results: []); // Simulate a host that says the solution is fully loaded. var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict); hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(() => IsFullyLoadedAsync(projectSystem: true, remoteHost: true)); hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(searchService.Object); var callbackMock = new Mock<INavigateToSearchCallback>(MockBehavior.Strict); callbackMock.Setup(c => c.ReportProgress(It.IsAny<int>(), It.IsAny<int>())); callbackMock.Setup(c => c.AddResultsAsync(results, It.IsAny<Document>(), It.IsAny<CancellationToken>())) .Returns(Task.CompletedTask); // Because we did a full search, we should let the user know it was totally accurate. callbackMock.Setup(c => c.Done(true)); var searcher = NavigateToSearcher.Create( workspace.CurrentSolution, callbackMock.Object, pattern, kinds: ImmutableHashSet<string>.Empty, hostMock.Object); await searcher.SearchAsync(NavigateToSearchScope.Solution, CancellationToken.None); } [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1933220")] public async Task DoNotCrashWithoutSearchService() { using var workspace = EditorTestWorkspace.CreateCSharp(""); var pattern = "irrelevant"; var results = ImmutableArray.Create<INavigateToSearchResult>(new TestNavigateToSearchResult(workspace, new TextSpan(0, 0))); var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict); hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(async () => true); // Ensure that returning null for the search service doesn't crash. hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(() => null); var callbackMock = new Mock<INavigateToSearchCallback>(MockBehavior.Strict); callbackMock.Setup(c => c.ReportIncomplete()); callbackMock.Setup(c => c.ReportProgress(It.IsAny<int>(), It.IsAny<int>())); callbackMock.Setup(c => c.AddResultsAsync(results, It.IsAny<Document>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask); callbackMock.Setup(c => c.Done(true)); var searcher = NavigateToSearcher.Create( workspace.CurrentSolution, callbackMock.Object, pattern, kinds: ImmutableHashSet<string>.Empty, hostMock.Object); await searcher.SearchAsync(NavigateToSearchScope.Solution, CancellationToken.None); } [Fact] public async Task ProjectScopeSearchingOnlySearchesSingleProjectForGeneratedDocuments() { using var workspace = EditorTestWorkspace.Create(""" <Workspace> <Project Language="C#" AssemblyName="Assembly1" CommonReferences="true"> <Document FilePath="z:\\file1.cs"> public class C { } </Document> </Project> <Project Language="C#" AssemblyName="Assembly2" CommonReferences="true"> <Document FilePath="z:\\file2.cs"> public class D { } </Document> </Project> </Workspace> """, composition: FirstActiveAndVisibleComposition); var pattern = "irrelevant"; var results = ImmutableArray.Create<INavigateToSearchResult>(new TestNavigateToSearchResult(workspace, new TextSpan(0, 0))); var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict); hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(async () => true); var searchGeneratedDocumentsAsyncCalled = false; var searchService = new MockAdvancedNavigateToSearchService { OnSearchGeneratedDocumentsAsyncCalled = () => { Assert.False(searchGeneratedDocumentsAsyncCalled); searchGeneratedDocumentsAsyncCalled = true; } }; // Ensure that returning null for the search service doesn't crash. hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(() => searchService); var callbackMock = new Mock<INavigateToSearchCallback>(MockBehavior.Strict); callbackMock.Setup(c => c.ReportIncomplete()); callbackMock.Setup(c => c.ReportProgress(It.IsAny<int>(), It.IsAny<int>())); callbackMock.Setup(c => c.AddResultsAsync(results, It.IsAny<Document>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask); callbackMock.Setup(c => c.Done(true)); var searcher = NavigateToSearcher.Create( workspace.CurrentSolution, callbackMock.Object, pattern, kinds: ImmutableHashSet<string>.Empty, hostMock.Object); // We're searching for a singular project, so we should only get a single call to search generated documents. await searcher.SearchAsync(NavigateToSearchScope.Project, CancellationToken.None); Assert.True(searchGeneratedDocumentsAsyncCalled); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/77051")] public async Task DocumentScopeRelatedDocuments_Inheritance() { using var workspace = EditorTestWorkspace.Create(""" <Workspace> <Project Language="C#" AssemblyName="Assembly1" CommonReferences="true"> <Document FilePath="z:\\file1.cs"> public class C : Base { // Starting search here. void Goo1() { } } </Document> <Document FilePath="z:\\file2.cs"> public class Base { // Should find this. void Goo2() { } } public class Other { // Should not find this. void Goo3() { } } </Document> </Project> </Workspace> """, composition: FirstActiveAndVisibleComposition); var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict); hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(async () => true); var project = workspace.CurrentSolution.Projects.Single(); var searchService = project.GetRequiredLanguageService<INavigateToSearchService>(); hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(() => searchService); var callback = new TestNavigateToSearchCallback(); var searcher = NavigateToSearcher.Create( workspace.CurrentSolution, callback, "Goo", kinds: searchService.KindsProvided, hostMock.Object); await searcher.SearchAsync(NavigateToSearchScope.Document, CancellationToken.None); Assert.Equal(2, callback.Results.Count); var firstDocument = project.Documents.Single(d => d.FilePath!.Contains("file1")); var secondDocument = project.Documents.Single(d => d.FilePath!.Contains("file2")); var firstDocumentResult = Assert.Single(callback.Results, r => r.NavigableItem.Document.Id == firstDocument.Id); var secondDocumentResult = Assert.Single(callback.Results, r => r.NavigableItem.Document.Id == secondDocument.Id); Assert.Equal("Goo1", firstDocumentResult.Name); Assert.Equal("Goo2", secondDocumentResult.Name); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/77051")] public async Task DocumentScopeRelatedDocuments_Partial() { using var workspace = EditorTestWorkspace.Create(""" <Workspace> <Project Language="C#" AssemblyName="Assembly1" CommonReferences="true"> <Document FilePath="z:\\file1.cs"> public partial class C { // Starting search here. void Goo1() { } } </Document> <Document FilePath="z:\\file2.cs"> public class Base { // Should not find this. void Goo2() { } } public partial class C { // Should find this. void Goo3() { } } </Document> </Project> </Workspace> """, composition: FirstActiveAndVisibleComposition); var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict); hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(async () => true); var project = workspace.CurrentSolution.Projects.Single(); var searchService = project.GetRequiredLanguageService<INavigateToSearchService>(); hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(() => searchService); var callback = new TestNavigateToSearchCallback(); var searcher = NavigateToSearcher.Create( workspace.CurrentSolution, callback, "Goo", kinds: searchService.KindsProvided, hostMock.Object); await searcher.SearchAsync(NavigateToSearchScope.Document, CancellationToken.None); Assert.Equal(2, callback.Results.Count); var firstDocument = project.Documents.Single(d => d.FilePath!.Contains("file1")); var secondDocument = project.Documents.Single(d => d.FilePath!.Contains("file2")); var firstDocumentResult = Assert.Single(callback.Results, r => r.NavigableItem.Document.Id == firstDocument.Id); var secondDocumentResult = Assert.Single(callback.Results, r => r.NavigableItem.Document.Id == secondDocument.Id); Assert.Equal("Goo1", firstDocumentResult.Name); Assert.Equal("Goo3", secondDocumentResult.Name); } private sealed class TestNavigateToSearchCallback : INavigateToSearchCallback { public readonly ConcurrentBag<INavigateToSearchResult> Results = []; public void Done(bool isFullyLoaded) { } public void ReportIncomplete() { } public async Task AddResultsAsync(ImmutableArray<INavigateToSearchResult> results, Document? activeDocument, CancellationToken cancellationToken) { foreach (var result in results) this.Results.Add(result); } public void ReportProgress(int current, int maximum) { } } private sealed class MockAdvancedNavigateToSearchService : IAdvancedNavigateToSearchService { public IImmutableSet<string> KindsProvided => AbstractNavigateToSearchService.AllKinds; public bool CanFilter => true; public Action? OnSearchCachedDocumentsAsyncCalled { get; set; } public Action? OnSearchDocumentsAsyncCalled { get; set; } public Action? OnSearchGeneratedDocumentsAsyncCalled { get; set; } public Action? OnSearchProjectsAsyncCalled { get; set; } public async Task SearchCachedDocumentsAsync(Solution solution, ImmutableArray<Project> projects, ImmutableArray<Document> priorityDocuments, string searchPattern, IImmutableSet<string> kinds, Document? activeDocument, Func<ImmutableArray<INavigateToSearchResult>, Task> onResultsFound, Func<Task> onProjectCompleted, CancellationToken cancellationToken) { OnSearchCachedDocumentsAsyncCalled?.Invoke(); } public async Task SearchDocumentAsync(Document document, string searchPattern, IImmutableSet<string> kinds, Func<ImmutableArray<INavigateToSearchResult>, Task> onResultsFound, CancellationToken cancellationToken) { OnSearchDocumentsAsyncCalled?.Invoke(); } public async Task SearchGeneratedDocumentsAsync(Solution solution, ImmutableArray<Project> projects, string searchPattern, IImmutableSet<string> kinds, Document? activeDocument, Func<ImmutableArray<INavigateToSearchResult>, Task> onResultsFound, Func<Task> onProjectCompleted, CancellationToken cancellationToken) { OnSearchGeneratedDocumentsAsyncCalled?.Invoke(); } public async Task SearchProjectsAsync(Solution solution, ImmutableArray<Project> projects, ImmutableArray<Document> priorityDocuments, string searchPattern, IImmutableSet<string> kinds, Document? activeDocument, Func<ImmutableArray<INavigateToSearchResult>, Task> onResultsFound, Func<Task> onProjectCompleted, CancellationToken cancellationToken) { OnSearchProjectsAsyncCalled?.Invoke(); } } private sealed class TestNavigateToSearchResult(EditorTestWorkspace workspace, TextSpan sourceSpan) : INavigateToSearchResult, INavigableItem { public INavigableItem.NavigableDocument Document => INavigableItem.NavigableDocument.FromDocument(workspace.CurrentSolution.Projects.Single().Documents.Single()); public TextSpan SourceSpan => sourceSpan; public string AdditionalInformation => throw new NotImplementedException(); public string Kind => throw new NotImplementedException(); public NavigateToMatchKind MatchKind => throw new NotImplementedException(); public bool IsCaseSensitive => throw new NotImplementedException(); public string Name => throw new NotImplementedException(); public ImmutableArray<TextSpan> NameMatchSpans => throw new NotImplementedException(); public string SecondarySort => throw new NotImplementedException(); public string Summary => throw new NotImplementedException(); public INavigableItem NavigableItem => this; public Glyph Glyph => throw new NotImplementedException(); public ImmutableArray<TaggedText> DisplayTaggedParts => throw new NotImplementedException(); public bool DisplayFileLocation => throw new NotImplementedException(); public bool IsImplicitlyDeclared => throw new NotImplementedException(); public bool IsStale => throw new NotImplementedException(); public ImmutableArray<INavigableItem> ChildItems => throw new NotImplementedException(); public ImmutableArray<PatternMatch> Matches => NavigateToSearchResultHelpers.GetMatches(this); } }