File: PdbSourceDocument\AbstractPdbSourceDocumentTests.cs
Web Access
Project: src\src\EditorFeatures\CSharpTest\Microsoft.CodeAnalysis.CSharp.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.EditorFeatures.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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.PdbSourceDocument;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument;
 
[UseExportProvider]
public abstract class AbstractPdbSourceDocumentTests
{
    public enum Location
    {
        OnDisk,
        Embedded
    }
 
    protected static Task TestAsync(
        Location pdbLocation,
        Location sourceLocation,
        string metadataSource,
        Func<Compilation, ISymbol> symbolMatcher,
        string[]? preprocessorSymbols = null,
        bool buildReferenceAssembly = false,
        bool expectNullResult = false)
    {
        return RunTestAsync(path => TestAsync(
            path,
            pdbLocation,
            sourceLocation,
            metadataSource,
            symbolMatcher,
            preprocessorSymbols,
            buildReferenceAssembly,
            expectNullResult));
    }
 
    protected static async Task RunTestAsync(Func<string, Task> testRunner)
    {
        var path = Path.Combine(Path.GetTempPath(), nameof(PdbSourceDocumentTests));
 
        try
        {
            Directory.CreateDirectory(path);
 
            await testRunner(path);
        }
        finally
        {
            if (Directory.Exists(path))
            {
                Directory.Delete(path, recursive: true);
            }
        }
    }
 
    protected static async Task TestAsync(
        string path,
        Location pdbLocation,
        Location sourceLocation,
        string metadataSource,
        Func<Compilation, ISymbol> symbolMatcher,
        string[]? preprocessorSymbols,
        bool buildReferenceAssembly,
        bool expectNullResult)
    {
        MarkupTestFile.GetSpan(metadataSource, out var source, out var expectedSpan);
 
        var (project, symbol) = await CompileAndFindSymbolAsync(
            path,
            pdbLocation,
            sourceLocation,
            source,
            symbolMatcher,
            preprocessorSymbols,
            buildReferenceAssembly,
            windowsPdb: false);
 
        await GenerateFileAndVerifyAsync(project, symbol, sourceLocation, source, expectedSpan, expectNullResult);
    }
 
    protected static async Task GenerateFileAndVerifyAsync(
        Project project,
        ISymbol symbol,
        Location sourceLocation,
        string expected,
        Text.TextSpan expectedSpan,
        bool expectNullResult)
    {
        var (actual, actualSpan) = await GetGeneratedSourceTextAsync(project, symbol, sourceLocation, expectNullResult);
 
        if (actual is null)
            return;
 
        // Compare exact texts and verify that the location returned is exactly that
        // indicated by expected
        AssertEx.EqualOrDiff(expected, actual.ToString());
        Assert.Equal(expectedSpan.Start, actualSpan.Start);
        Assert.Equal(expectedSpan.End, actualSpan.End);
    }
 
    protected static async Task<(SourceText?, TextSpan)> GetGeneratedSourceTextAsync(
        Project project,
        ISymbol symbol,
        Location sourceLocation,
        bool expectNullResult)
    {
        using var workspace = (EditorTestWorkspace)project.Solution.Workspace;
 
        var service = workspace.GetService<IMetadataAsSourceFileService>();
        try
        {
            // Using default settings here because none of the tests exercise any of the settings
            var file = await service.GetGeneratedFileAsync(workspace, project, symbol, signaturesOnly: false, options: MetadataAsSourceOptions.Default, cancellationToken: CancellationToken.None).ConfigureAwait(false);
 
            if (expectNullResult)
            {
                Assert.Same(NullResultMetadataAsSourceFileProvider.NullResult, file);
                return (null, default);
            }
            else
            {
                Assert.NotSame(NullResultMetadataAsSourceFileProvider.NullResult, file);
            }
 
            if (sourceLocation == Location.OnDisk)
            {
                Assert.True(file.DocumentTitle.Contains($"[{FeaturesResources.external}]"));
            }
            else
            {
                Assert.True(file.DocumentTitle.Contains($"[{FeaturesResources.embedded}]"));
            }
 
            AssertEx.NotNull(file, $"No source document was found in the pdb for the symbol.");
 
            var masWorkspace = service.TryGetWorkspace();
 
            var pdbService = (PdbSourceDocumentMetadataAsSourceFileProvider)workspace.ExportProvider.GetExportedValues<IMetadataAsSourceFileProvider>().Single(s => s is PdbSourceDocumentMetadataAsSourceFileProvider);
 
            // Add the document to the workspace.  We provide an empty static source text as the API requires it to open the document.
            // We're not really trying to verify that the source text the editor hands to us is the right encoding - just that the document we added has the right encoding.
            var result = pdbService.TryAddDocumentToWorkspace((MetadataAsSourceWorkspace)masWorkspace!, file.FilePath, new StaticSourceTextContainer(SourceText.From(string.Empty)), out _);
            Assert.True(result);
 
            // Immediately close the document so that we get the source text provided by the workspace (instead of the empty one we passed).
            var info = pdbService.GetTestAccessor().Documents[file.FilePath];
            masWorkspace!.OnDocumentClosed(info.DocumentId, new WorkspaceFileTextLoader(workspace.Services.SolutionServices, file.FilePath, info.Encoding));
 
            var document = masWorkspace!.CurrentSolution.GetRequiredDocument(info.DocumentId);
 
            // Mapping the project from the generated document should map back to the original project
            var provider = workspace.ExportProvider.GetExportedValues<IMetadataAsSourceFileProvider>().OfType<PdbSourceDocumentMetadataAsSourceFileProvider>().Single();
            var mappedProject = provider.MapDocument(document);
            Assert.NotNull(mappedProject);
            Assert.Equal(project.Id, mappedProject!.Id);
 
            var actual = await document.GetTextAsync();
            var actualSpan = file!.IdentifierLocation.SourceSpan;
 
            return (actual, actualSpan);
        }
        finally
        {
            service.TryGetWorkspace()?.Dispose();
        }
    }
 
    protected static Task<(Project, ISymbol)> CompileAndFindSymbolAsync(
        string path,
        Location pdbLocation,
        Location sourceLocation,
        string source,
        Func<Compilation, ISymbol> symbolMatcher,
        string[]? preprocessorSymbols = null,
        bool buildReferenceAssembly = false,
        bool windowsPdb = false,
        Encoding? encoding = null)
    {
        var sourceText = SourceText.From(source, encoding: encoding ?? Encoding.UTF8);
        return CompileAndFindSymbolAsync(path, pdbLocation, sourceLocation, sourceText, symbolMatcher, preprocessorSymbols, buildReferenceAssembly, windowsPdb);
    }
 
    protected static async Task<(Project, ISymbol)> CompileAndFindSymbolAsync(
        string path,
        Location pdbLocation,
        Location sourceLocation,
        SourceText source,
        Func<Compilation, ISymbol> symbolMatcher,
        string[]? preprocessorSymbols = null,
        bool buildReferenceAssembly = false,
        bool windowsPdb = false,
        Encoding? fallbackEncoding = null)
    {
        var preprocessorSymbolsAttribute = preprocessorSymbols?.Length > 0
            ? $"PreprocessorSymbols=\"{string.Join(";", preprocessorSymbols)}\""
            : "";
 
        var workspace = EditorTestWorkspace.Create(@$"
<Workspace>
    <Project Language=""{LanguageNames.CSharp}"" CommonReferences=""true"" ReferencesOnDisk=""true"" {preprocessorSymbolsAttribute}>
    </Project>
</Workspace>", composition: GetTestComposition());
 
        var project = workspace.CurrentSolution.Projects.First();
 
        CompileTestSource(path, source, project, pdbLocation, sourceLocation, buildReferenceAssembly, windowsPdb, fallbackEncoding);
 
        project = project.AddMetadataReference(MetadataReference.CreateFromFile(GetDllPath(path)));
 
        var mainCompilation = await project.GetRequiredCompilationAsync(CancellationToken.None).ConfigureAwait(false);
 
        var symbol = symbolMatcher(mainCompilation);
 
        AssertEx.NotNull(symbol, $"Couldn't find symbol to go-to-def for.");
 
        return (project, symbol);
    }
 
    protected static TestComposition GetTestComposition()
    {
        // We construct our own composition here because we only want the decompilation metadata as source provider
        // to be available.
 
        return EditorTestCompositions.EditorFeatures
            .WithExcludedPartTypes(ImmutableHashSet.Create(typeof(IMetadataAsSourceFileProvider)))
            .AddParts(typeof(PdbSourceDocumentMetadataAsSourceFileProvider), typeof(NullResultMetadataAsSourceFileProvider));
    }
 
    protected static void CompileTestSource(string path, SourceText source, Project project, Location pdbLocation, Location sourceLocation, bool buildReferenceAssembly, bool windowsPdb, Encoding? fallbackEncoding = null)
    {
        var dllFilePath = GetDllPath(path);
        var sourceCodePath = GetSourceFilePath(path);
        var pdbFilePath = GetPdbPath(path);
        var assemblyName = "reference";
 
        CompileTestSource(dllFilePath, sourceCodePath, pdbFilePath, assemblyName, source, project, pdbLocation, sourceLocation, buildReferenceAssembly, windowsPdb, fallbackEncoding);
    }
 
    protected static void CompileTestSource(string dllFilePath, string sourceCodePath, string? pdbFilePath, string assemblyName, SourceText source, Project project, Location pdbLocation, Location sourceLocation, bool buildReferenceAssembly, bool windowsPdb, Encoding? fallbackEncoding = null)
    {
        CompileTestSource(dllFilePath, [sourceCodePath], pdbFilePath, assemblyName, [source], project, pdbLocation, sourceLocation, buildReferenceAssembly, windowsPdb, fallbackEncoding);
    }
 
    protected static void CompileTestSource(string dllFilePath, string[] sourceCodePaths, string? pdbFilePath, string assemblyName, SourceText[] sources, Project project, Location pdbLocation, Location sourceLocation, bool buildReferenceAssembly, bool windowsPdb, Encoding? fallbackEncoding = null)
    {
        var compilationFactory = project.Solution.Services.GetRequiredLanguageService<ICompilationFactoryService>(LanguageNames.CSharp);
        var options = compilationFactory.GetDefaultCompilationOptions().WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
        var parseOptions = project.ParseOptions;
 
        var compilation = compilationFactory
            .CreateCompilation(assemblyName, options)
            .AddSyntaxTrees(sources.Select((s, i) => SyntaxFactory.ParseSyntaxTree(s, options: parseOptions, path: sourceCodePaths[i])))
            .AddReferences(project.MetadataReferences);
 
        IEnumerable<EmbeddedText>? embeddedTexts;
        if (buildReferenceAssembly)
        {
            embeddedTexts = null;
        }
        else if (sourceLocation == Location.OnDisk)
        {
            embeddedTexts = null;
            for (var i = 0; i < sources.Length; i++)
            {
                File.WriteAllText(sourceCodePaths[i], sources[i].ToString(), sources[i].Encoding);
            }
        }
        else
        {
            embeddedTexts = sources.Select((s, i) => EmbeddedText.FromSource(sourceCodePaths[i], s)).ToArray();
        }
 
        EmitOptions emitOptions;
        if (buildReferenceAssembly)
        {
            pdbFilePath = null;
            emitOptions = new EmitOptions(metadataOnly: true, includePrivateMembers: false);
        }
        else if (pdbLocation == Location.OnDisk)
        {
            emitOptions = new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb, pdbFilePath: pdbFilePath);
        }
        else
        {
            pdbFilePath = null;
            emitOptions = new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded);
        }
 
        // TODO: When supported, move this to pdbLocation
        if (windowsPdb)
        {
            emitOptions = emitOptions.WithDebugInformationFormat(DebugInformationFormat.Pdb);
        }
 
        if (fallbackEncoding is null)
        {
            emitOptions = emitOptions.WithDefaultSourceFileEncoding(sources[0].Encoding);
        }
        else
        {
            emitOptions = emitOptions.WithFallbackSourceFileEncoding(fallbackEncoding);
        }
 
        using var dllStream = FileUtilities.CreateFileStreamChecked(File.Create, dllFilePath, nameof(dllFilePath));
        using var pdbStream = (pdbFilePath == null ? null : FileUtilities.CreateFileStreamChecked(File.Create, pdbFilePath, nameof(pdbFilePath)));
        var result = compilation.Emit(dllStream, pdbStream, options: emitOptions, embeddedTexts: embeddedTexts);
        Assert.Empty(result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
    }
 
    protected static string GetDllPath(string path)
    {
        return Path.Combine(path, "reference.dll");
    }
 
    protected static string GetSourceFilePath(string path)
    {
        return Path.Combine(path, "source.cs");
    }
 
    protected static string GetPdbPath(string path)
    {
        return Path.Combine(path, "reference.pdb");
    }
 
    protected class StaticSourceTextContainer(SourceText sourceText) : SourceTextContainer
    {
        public override SourceText CurrentText => sourceText;
 
        public override event EventHandler<TextChangeEventArgs> TextChanged
        {
            add { }
            remove { }
        }
    }
}