-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #58061 from dotnet/merges/release/dev17.1-to-main
Merge release/dev17.1 to main
- Loading branch information
Showing
20 changed files
with
1,100 additions
and
462 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
303 changes: 303 additions & 0 deletions
303
src/EditorFeatures/CSharpTest/PdbSourceDocument/AbstractPdbSourceDocumentTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,303 @@ | ||
// 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.Editor.UnitTests.Workspaces; | ||
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, source, expectedSpan, expectNullResult); | ||
} | ||
|
||
protected static async Task GenerateFileAndVerifyAsync( | ||
Project project, | ||
ISymbol symbol, | ||
string expected, | ||
Text.TextSpan expectedSpan, | ||
bool expectNullResult) | ||
{ | ||
var (actual, actualSpan) = await GetGeneratedSourceTextAsync(project, symbol, 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, | ||
bool expectNullResult) | ||
{ | ||
using var workspace = (TestWorkspace)project.Solution.Workspace; | ||
|
||
var service = workspace.GetService<IMetadataAsSourceFileService>(); | ||
try | ||
{ | ||
var file = await service.GetGeneratedFileAsync(project, symbol, signaturesOnly: false, allowDecompilation: false, CancellationToken.None).ConfigureAwait(false); | ||
|
||
if (expectNullResult) | ||
{ | ||
Assert.Same(NullResultMetadataAsSourceFileProvider.NullResult, file); | ||
return (null, default); | ||
} | ||
else | ||
{ | ||
Assert.NotSame(NullResultMetadataAsSourceFileProvider.NullResult, file); | ||
} | ||
|
||
AssertEx.NotNull(file, $"No source document was found in the pdb for the symbol."); | ||
|
||
var masWorkspace = service.TryGetWorkspace(); | ||
|
||
var document = masWorkspace!.CurrentSolution.Projects.First().Documents.First(); | ||
|
||
Assert.Equal(document.FilePath, file.FilePath); | ||
|
||
var actual = await document.GetTextAsync(); | ||
var actualSpan = file!.IdentifierLocation.SourceSpan; | ||
|
||
return (actual, actualSpan); | ||
} | ||
finally | ||
{ | ||
service.CleanupGeneratedFiles(); | ||
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)}\"" | ||
: ""; | ||
|
||
// We construct our own composition here because we only want the decompilation metadata as source provider | ||
// to be available. | ||
var composition = EditorTestCompositions.EditorFeatures | ||
.WithExcludedPartTypes(ImmutableHashSet.Create(typeof(IMetadataAsSourceFileProvider))) | ||
.AddParts(typeof(PdbSourceDocumentMetadataAsSourceFileProvider), typeof(NullResultMetadataAsSourceFileProvider)); | ||
|
||
var workspace = TestWorkspace.Create(@$" | ||
<Workspace> | ||
<Project Language=""{LanguageNames.CSharp}"" CommonReferences=""true"" ReferencesOnDisk=""true"" {preprocessorSymbolsAttribute}> | ||
</Project> | ||
</Workspace>", composition: composition); | ||
|
||
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 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 = "ReferencedAssembly"; | ||
|
||
var languageServices = project.Solution.Workspace.Services.GetLanguageServices(LanguageNames.CSharp); | ||
var compilationFactory = languageServices.GetRequiredService<ICompilationFactoryService>(); | ||
var options = compilationFactory.GetDefaultCompilationOptions().WithOutputKind(OutputKind.DynamicallyLinkedLibrary); | ||
var parseOptions = project.ParseOptions; | ||
|
||
var compilation = compilationFactory | ||
.CreateCompilation(assemblyName, options) | ||
.AddSyntaxTrees(SyntaxFactory.ParseSyntaxTree(source, options: parseOptions, path: sourceCodePath)) | ||
.AddReferences(project.MetadataReferences); | ||
|
||
IEnumerable<EmbeddedText>? embeddedTexts; | ||
if (sourceLocation == Location.OnDisk) | ||
{ | ||
embeddedTexts = null; | ||
File.WriteAllText(sourceCodePath, source.ToString(), source.Encoding); | ||
} | ||
else | ||
{ | ||
embeddedTexts = new[] { EmbeddedText.FromSource(sourceCodePath, source) }; | ||
} | ||
|
||
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(source.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"); | ||
} | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
src/EditorFeatures/CSharpTest/PdbSourceDocument/NullResultMetadataAsSourceFileProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// 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.Composition; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis.Host.Mef; | ||
using Microsoft.CodeAnalysis.MetadataAsSource; | ||
using Microsoft.CodeAnalysis.PdbSourceDocument; | ||
|
||
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.PdbSourceDocument | ||
{ | ||
/// <summary> | ||
/// IMetadataAsSourceFileService has to always return a result, but for our testing | ||
/// we remove the decompilation provider that would normally ensure that. This provider | ||
/// takes it place to ensure we always return a known null result, so we can also verify | ||
/// against it in tests. | ||
/// </summary> | ||
[ExportMetadataAsSourceFileProvider("Dummy"), Shared] | ||
[ExtensionOrder(After = PdbSourceDocumentMetadataAsSourceFileProvider.ProviderName)] | ||
internal class NullResultMetadataAsSourceFileProvider : IMetadataAsSourceFileProvider | ||
{ | ||
// Represents a null result | ||
public static MetadataAsSourceFile NullResult = new("", null, null, null); | ||
|
||
[ImportingConstructor] | ||
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
public NullResultMetadataAsSourceFileProvider() | ||
{ | ||
} | ||
|
||
public void CleanupGeneratedFiles(Workspace? workspace) | ||
{ | ||
} | ||
|
||
public Task<MetadataAsSourceFile?> GetGeneratedFileAsync(Workspace workspace, Project project, ISymbol symbol, bool signaturesOnly, bool allowDecompilation, string tempPath, CancellationToken cancellationToken) | ||
{ | ||
return Task.FromResult<MetadataAsSourceFile?>(NullResult); | ||
} | ||
|
||
public Project? MapDocument(Document document) | ||
{ | ||
return null; | ||
} | ||
|
||
public bool TryAddDocumentToWorkspace(Workspace workspace, string filePath, Text.SourceTextContainer sourceTextContainer) | ||
{ | ||
return true; | ||
} | ||
|
||
public bool TryRemoveDocumentFromWorkspace(Workspace workspace, string filePath) | ||
{ | ||
return true; | ||
} | ||
} | ||
} |
Oops, something went wrong.