File: Rename\RenameEngineResult.vb
Web Access
Project: src\src\EditorFeatures\Test2\Microsoft.CodeAnalysis.EditorFeatures2.UnitTests.vbproj (Microsoft.CodeAnalysis.EditorFeatures2.UnitTests)
' 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.
 
Imports System.Collections.Immutable
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.CodeActions
Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
Imports Microsoft.CodeAnalysis.Host
Imports Microsoft.CodeAnalysis.Remote.Testing
Imports Microsoft.CodeAnalysis.Rename
Imports Microsoft.CodeAnalysis.Rename.ConflictEngine
Imports Microsoft.CodeAnalysis.Text
Imports Microsoft.VisualStudio.Text
Imports Xunit.Abstractions
Imports Xunit.Sdk
 
Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Rename
    ''' <summary>
    ''' A class that holds the result of a rename engine call, and asserts
    ''' various things about it. This is used in the tests for the rename engine
    ''' to make asserting that certain spans were converted to what they should be.
    ''' </summary>
    Friend Class RenameEngineResult
        Implements IDisposable
 
        Private ReadOnly _workspace As TestWorkspace
        Private ReadOnly _resolution As ConflictResolution
 
        ''' <summary>
        ''' The list of related locations that haven't been asserted about yet. Items are
        ''' removed from here when they are asserted on, so the set should be empty once we're
        ''' done.
        ''' </summary>
        Private ReadOnly _unassertedRelatedLocations As HashSet(Of RelatedLocation)
 
        Private ReadOnly _renameTo As String
 
        Private _failedAssert As Boolean
 
        Private Sub New(workspace As TestWorkspace, resolution As ConflictResolution, renameTo As String)
            _workspace = workspace
            _resolution = resolution
 
            _unassertedRelatedLocations = New HashSet(Of RelatedLocation)(resolution.RelatedLocations)
 
            _renameTo = renameTo
        End Sub
 
        Public Shared Function Create(
                helper As ITestOutputHelper,
                workspaceXml As XElement,
                renameTo As String,
                host As RenameTestHost,
                Optional renameOptions As SymbolRenameOptions = Nothing,
                Optional expectFailure As Boolean = False,
                Optional sourceGenerator As ISourceGenerator = Nothing,
                Optional executionPreference As SourceGeneratorExecutionPreference = SourceGeneratorExecutionPreference.Automatic) As RenameEngineResult
 
            Dim composition = EditorTestCompositions.EditorFeatures.AddParts(
                GetType(NoCompilationContentTypeLanguageService),
                GetType(NoCompilationContentTypeDefinitions),
                GetType(WorkspaceTestLogger),
                GetType(TestWorkspaceConfigurationService))
 
            If host = RenameTestHost.OutOfProcess_SingleCall OrElse host = RenameTestHost.OutOfProcess_SplitCall Then
                composition = composition.WithTestHostParts(TestHost.OutOfProcess)
            End If
 
            Dim workspace = TestWorkspace.CreateWorkspace(workspaceXml, composition:=composition)
 
            Dim configService = workspace.ExportProvider.GetExportedValue(Of TestWorkspaceConfigurationService)
            configService.Options = New WorkspaceConfigurationOptions(SourceGeneratorExecution:=executionPreference)
 
            workspace.Services.SolutionServices.SetWorkspaceTestOutput(helper)
 
            If sourceGenerator IsNot Nothing Then
                workspace.OnAnalyzerReferenceAdded(workspace.CurrentSolution.ProjectIds.Single(), New TestGeneratorReference(sourceGenerator))
            End If
 
            Dim success = False
            Dim engineResult As RenameEngineResult = Nothing
            Try
                If workspace.Documents.Where(Function(d) d.CursorPosition.HasValue).Count <> 1 Then
                    AssertEx.Fail("The test must have a single $$ marking the symbol being renamed.")
                End If
 
                Dim cursorDocument = workspace.Documents.Single(Function(d) d.CursorPosition.HasValue)
                Dim cursorPosition = cursorDocument.CursorPosition.Value
 
                Dim document = workspace.CurrentSolution.GetDocument(cursorDocument.Id)
 
                Dim symbol = RenameUtilities.TryGetRenamableSymbolAsync(document, cursorPosition, CancellationToken.None).Result
                If symbol Is Nothing Then
                    AssertEx.Fail("The symbol touching the $$ could not be found.")
                End If
 
                Dim result = GetConflictResolution(renameTo, workspace.CurrentSolution, symbol, renameOptions, host)
 
                If expectFailure Then
                    Assert.False(result.IsSuccessful)
                    Assert.NotNull(result.ErrorMessage)
                    Return engineResult
                Else
                    Assert.Null(result.ErrorMessage)
                End If
 
                engineResult = New RenameEngineResult(workspace, result, renameTo)
                engineResult.AssertUnlabeledSpansRenamedAndHaveNoConflicts()
                success = True
            Finally
                If Not success Then
                    ' Something blew up, so we still own the test workspace
                    If engineResult IsNot Nothing Then
                        engineResult.Dispose()
                    Else
                        workspace.Dispose()
                    End If
                End If
            End Try
 
            Return engineResult
        End Function
 
        Private Shared Function GetConflictResolution(
                renameTo As String,
                solution As Solution,
                symbol As ISymbol,
                renameOptions As SymbolRenameOptions,
                host As RenameTestHost) As ConflictResolution
 
            If host = RenameTestHost.OutOfProcess_SplitCall Then
                ' This tests that each portion of rename can properly marshal to/from the OOP process. It validates
                ' features that need to call each part independently and operate on the intermediary values.
 
                Dim locations = Renamer.FindRenameLocationsAsync(
                    solution, symbol, renameOptions, CancellationToken.None).GetAwaiter().GetResult()
 
                Return locations.ResolveConflictsAsync(symbol, renameTo, nonConflictSymbolKeys:=Nothing, CancellationToken.None).GetAwaiter().GetResult()
            Else
                ' This tests that rename properly works when the entire call is remoted to OOP and the final result is
                ' marshaled back.
 
                Return Renamer.RenameSymbolAsync(
                    solution, symbol, renameTo, renameOptions,
                    nonConflictSymbolKeys:=Nothing, CancellationToken.None).GetAwaiter().GetResult()
            End If
        End Function
 
        Friend ReadOnly Property ConflictResolution As ConflictResolution
            Get
                Return _resolution
            End Get
        End Property
 
        Private Sub AssertUnlabeledSpansRenamedAndHaveNoConflicts()
            For Each documentWithSpans In _workspace.Documents.Where(Function(d) Not d.IsSourceGenerated)
                Dim oldSyntaxTree = _workspace.CurrentSolution.GetDocument(documentWithSpans.Id).GetSyntaxTreeAsync().Result
 
                For Each span In documentWithSpans.SelectedSpans
                    Dim location = oldSyntaxTree.GetLocation(span)
 
                    AssertLocationReferencedAs(location, RelatedLocationType.NoConflict)
                    AssertLocationReplacedWith(location, _renameTo)
                Next
            Next
        End Sub
 
        Public Sub AssertLabeledSpansInStringsAndCommentsAre(label As String, replacement As String)
            AssertLabeledSpansAre(label, replacement, RelatedLocationType.NoConflict, isRenameWithinStringOrComment:=True)
        End Sub
 
        Public Sub AssertLabeledSpansAre(label As String, Optional replacement As String = Nothing, Optional type As RelatedLocationType? = Nothing, Optional isRenameWithinStringOrComment As Boolean = False)
            For Each location In GetLabeledLocations(label)
                If replacement IsNot Nothing Then
                    If type.HasValue Then
                        AssertLocationReplacedWith(location, replacement, isRenameWithinStringOrComment)
                    End If
                End If
 
                If type.HasValue AndAlso Not isRenameWithinStringOrComment Then
                    AssertLocationReferencedAs(location, type.Value)
                End If
 
            Next
        End Sub
 
        Public Sub AssertLabeledSpecialSpansAre(label As String, replacement As String, type As RelatedLocationType?)
            For Each location In GetLabeledLocations(label)
                If replacement IsNot Nothing Then
                    If type.HasValue Then
                        AssertLocationReplacedWith(location, replacement)
                        AssertLocationReferencedAs(location, type.Value)
                    End If
                End If
            Next
        End Sub
 
        Private Function GetLabeledLocations(label As String) As IEnumerable(Of Location)
            Dim locations As New List(Of Location)
 
            For Each document In _workspace.Documents
                Dim annotatedSpans = document.AnnotatedSpans
 
                Dim spans As ImmutableArray(Of TextSpan) = Nothing
                If annotatedSpans.TryGetValue(label, spans) Then
                    Dim syntaxTree = _workspace.CurrentSolution.GetDocument(document.Id).GetSyntaxTreeAsync().Result
 
                    For Each span In spans
                        locations.Add(syntaxTree.GetLocation(span))
                    Next
                End If
            Next
 
            If locations.Count = 0 Then
                _failedAssert = True
                AssertEx.Fail(String.Format("The label '{0}' was not mentioned in the test.", label))
            End If
 
            Return locations
        End Function
 
        Private Sub AssertLocationReplacedWith(location As Location, replacementText As String, Optional isRenameWithinStringOrComment As Boolean = False)
            Try
                Dim documentId = ConflictResolution.OldSolution.GetDocumentId(location.SourceTree)
                Dim newLocation = ConflictResolution.GetResolutionTextSpan(location.SourceSpan, documentId)
 
                Dim newTree = ConflictResolution.NewSolution.GetDocument(documentId).GetSyntaxTreeAsync().Result
                Dim newToken = newTree.GetRoot.FindToken(newLocation.Start, findInsideTrivia:=True)
                Dim newText As String
                If newToken.Span = newLocation Then
                    newText = newToken.ToString()
                ElseIf isRenameWithinStringOrComment AndAlso newToken.FullSpan.Contains(newLocation) Then
                    newText = newToken.ToFullString().Substring(newLocation.Start - newToken.FullSpan.Start, newLocation.Length)
                Else
                    newText = newTree.GetText().ToString(newLocation)
                End If
 
                Assert.Equal(replacementText, newText)
            Catch ex As XunitException
                _failedAssert = True
                Throw
            End Try
        End Sub
 
        Private Sub AssertLocationReferencedAs(location As Location, type As RelatedLocationType)
            Try
                Dim documentId = ConflictResolution.OldSolution.GetDocumentId(location.SourceTree)
                Dim reference = _unassertedRelatedLocations.SingleOrDefault(
                    Function(r) r.ConflictCheckSpan = location.SourceSpan AndAlso r.DocumentId = documentId)
 
                Assert.NotNull(reference)
                Assert.True(type.HasFlag(reference.Type))
 
                _unassertedRelatedLocations.Remove(reference)
            Catch ex As XunitException
                _failedAssert = True
                Throw
            End Try
        End Sub
 
        Private Sub Dispose() Implements IDisposable.Dispose
            ' Make sure we're cleaned up. Don't want the test harness crashing...
            GC.SuppressFinalize(Me)
 
            ' If we failed some other assert, we know we're going to have things left
            ' over. So let's just suppress these so we don't lose the root cause
            If Not _failedAssert Then
                If _unassertedRelatedLocations.Count > 0 Then
                    AssertEx.Fail(
                        "There were additional related locations that were unasserted:" + Environment.NewLine _
                        + String.Join(Environment.NewLine,
                            From location In _unassertedRelatedLocations
                            Let document = _workspace.CurrentSolution.GetDocument(location.DocumentId)
                            Let spanText = document.GetTextSynchronously(CancellationToken.None).ToString(location.ConflictCheckSpan)
                            Select $"{spanText} @{document.Name}[{location.ConflictCheckSpan.Start}..{location.ConflictCheckSpan.End})"))
                End If
            End If
 
            _workspace.Dispose()
        End Sub
 
        Protected Overrides Sub Finalize()
            If Not Environment.HasShutdownStarted Then
                Throw New Exception("Dispose was not called in a Rename test.")
            End If
        End Sub
 
        Public Sub AssertReplacementTextInvalid()
            Try
                Assert.False(_resolution.ReplacementTextValid)
            Catch ex As XunitException
                _failedAssert = True
                Throw
            End Try
        End Sub
 
        Public Sub AssertIsInvalidResolution()
            Assert.Null(_resolution)
        End Sub
 
    End Class
End Namespace