|
// 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.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Rebuild;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.Extensions.Logging;
using Microsoft.Metadata.Tools;
using Roslyn.Utilities;
using Xunit;
using Roslyn.Test.Utilities;
using System.Collections.Generic;
namespace Microsoft.CodeAnalysis.Rebuild.UnitTests
{
internal static class RoundTripUtil
{
public static void VerifyRoundTrip(
MemoryStream peStream,
MemoryStream? pdbStream,
string assemblyFileName,
IRebuildArtifactResolver rebuildArtifactResolver,
CancellationToken cancellationToken = default)
{
using var peReader = new PEReader(peStream);
var embeddedPdbReader = peReader.GetEmbeddedPdbMetadataReader();
var portablePdbReader = pdbStream is not null ? MetadataReaderProvider.FromPortablePdbStream(pdbStream).GetMetadataReader() : null;
Assert.True(embeddedPdbReader == null ^ portablePdbReader == null);
var pdbReader = embeddedPdbReader ?? portablePdbReader ?? throw ExceptionUtilities.Unreachable();
var factory = LoggerFactory.Create(configure => { });
var logger = factory.CreateLogger("RoundTripVerification");
var optionsReader = new CompilationOptionsReader(logger, pdbReader, peReader);
var compilationFactory = CompilationFactory.Create(
assemblyFileName,
optionsReader);
using var rebuildPeStream = new MemoryStream();
using var rebuildPdbStream = optionsReader.HasEmbeddedPdb ? null : new MemoryStream();
var emitResult = compilationFactory.Emit(
rebuildPeStream,
rebuildPdbStream,
rebuildArtifactResolver,
cancellationToken);
Assert.True(emitResult.Success);
Assert.Equal(peStream.ToArray(), rebuildPeStream.ToArray());
Assert.Equal(pdbStream?.ToArray(), rebuildPdbStream?.ToArray());
}
private record EmitInfo(ImmutableArray<byte> PEBytes, PEReader PEReader, ImmutableArray<byte> PdbBytes, MetadataReader PdbReader) : IDisposable
{
public void Dispose() => PEReader.Dispose();
}
public static unsafe void VerifyRoundTrip<TCompilation>(TCompilation original, EmitOptions? emitOptions = null)
where TCompilation : Compilation
{
Assert.True(original.SyntaxTrees.All(x => !string.IsNullOrEmpty(x.FilePath)));
Assert.True(original.Options.Deterministic);
original.VerifyDiagnostics();
emitOptions ??= new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded);
using var originalEmit = emitOriginal();
var factory = LoggerFactory.Create(configure => { });
var logger = factory.CreateLogger("RoundTripVerification");
var optionsReader = new CompilationOptionsReader(logger, originalEmit.PdbReader, originalEmit.PEReader);
var assemblyFileName = original.AssemblyName!;
if (typeof(TCompilation) == typeof(CSharpCompilation))
{
var assemblyFileExtension = original.Options.OutputKind switch
{
OutputKind.DynamicallyLinkedLibrary => ".dll",
OutputKind.ConsoleApplication => ".exe",
_ => throw new InvalidOperationException(),
};
assemblyFileName += assemblyFileExtension;
}
var compilationFactory = CompilationFactory.Create(assemblyFileName, optionsReader);
var rebuild = compilationFactory.CreateCompilation(
original.SyntaxTrees.SelectAsArray(x => compilationFactory.CreateSyntaxTree(x.FilePath, x.GetText())),
original.References.ToImmutableArray());
Assert.IsType<TCompilation>(rebuild);
VerifyCompilationOptions(original.Options, rebuild.Options);
using var rebuildStream = new MemoryStream();
using var rebuildPdbStream = optionsReader.HasEmbeddedPdb ? null : new MemoryStream();
var result = compilationFactory.Emit(
rebuildStream,
rebuildPdbStream,
rebuild,
embeddedTexts: ImmutableArray<EmbeddedText>.Empty,
CancellationToken.None);
Assert.Empty(result.Diagnostics);
Assert.True(result.Success);
var rebuildBytes = rebuildStream.ToImmutable();
using var rebuildReader = new PEReader(rebuildBytes);
ImmutableArray<byte> rebuildPdbBytes;
MetadataReader rebuildPdbReader;
if (rebuildPdbStream is null)
{
rebuildPdbReader = rebuildReader.GetEmbeddedPdbMetadataReader() ?? throw new InvalidOperationException();
rebuildPdbBytes = new ReadOnlySpan<byte>(rebuildPdbReader.MetadataPointer, rebuildPdbReader.MetadataLength).ToArray().ToImmutableArray();
}
else
{
rebuildPdbBytes = rebuildPdbStream.ToImmutable();
rebuildPdbReader = MetadataReaderProvider.FromPortablePdbImage(rebuildPdbBytes).GetMetadataReader();
}
var rebuildEmit = new EmitInfo(rebuildBytes, rebuildReader, rebuildPdbBytes, rebuildPdbReader);
// https://github.com/dotnet/roslyn/issues/52327
// This should be replaced with a CompilationDiff-based helper in MS.CA.RB which writes diffs
// out to the test output, not necessarily entire visualizations.
AssertImagesEqual(originalEmit, rebuildEmit);
unsafe EmitInfo emitOriginal()
{
switch (emitOptions.DebugInformationFormat)
{
case DebugInformationFormat.Embedded:
{
var originalBytes = original.EmitToArray(emitOptions);
var originalReader = new PEReader(originalBytes);
var originalPdbReader = originalReader.GetEmbeddedPdbMetadataReader();
Assert.NotNull(originalPdbReader);
var pdbSpan = new ReadOnlySpan<byte>(originalPdbReader!.MetadataPointer, originalPdbReader.MetadataLength);
return new EmitInfo(originalBytes, originalReader, pdbSpan.ToArray().ToImmutableArray(), originalPdbReader);
}
case DebugInformationFormat.PortablePdb:
{
using var pdbStream = new MemoryStream();
var originalBytes = original.EmitToArray(emitOptions, pdbStream: pdbStream);
var originalReader = new PEReader(originalBytes);
var pdbBytes = pdbStream.ToImmutable();
var originalPdbReader = MetadataReaderProvider.FromPortablePdbImage(pdbBytes).GetMetadataReader();
return new EmitInfo(originalBytes, originalReader, pdbBytes, originalPdbReader);
}
default:
throw new ArgumentException("Unsupported DebugInformationFormat: " + emitOptions.DebugInformationFormat);
}
}
}
private static void AssertImagesEqual(
EmitInfo originalEmit,
EmitInfo rebuildEmit)
{
if (!originalEmit.PEBytes.SequenceEqual(rebuildEmit.PEBytes))
{
var originalMdv = getMdv(originalEmit.PEReader.GetMetadataReader());
var rebuildMdv = getMdv(rebuildEmit.PEReader.GetMetadataReader());
// At this point the bytes were not equal, so the MDV output should also not be equal.
Assert.NotEqual(originalMdv, rebuildMdv);
// https://github.com/dotnet/roslyn/issues/52327
// this is not all that useful without manual copy/pasting. Can we diff this during the test and show the differences?
Assert.True(false, $@"
Expected:
{originalMdv}
Actual:
{rebuildMdv}
");
}
if (!originalEmit.PdbBytes.SequenceEqual(rebuildEmit.PdbBytes))
{
var originalMdv = getMdv(originalEmit.PdbReader);
var rebuildMdv = getMdv(rebuildEmit.PdbReader);
Assert.True(false, $@"
Expected:
{originalMdv}
Actual:
{rebuildMdv}
");
}
static string getMdv(MetadataReader metadataReader)
{
using var stream = new MemoryStream();
var writer = new StreamWriter(stream);
var visualizer = new MetadataVisualizer(metadataReader, writer);
visualizer.Visualize();
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}
}
#pragma warning disable 612 // 'CompilationOptions.Features' is obsolete
public static void VerifyCompilationOptions(CompilationOptions originalOptions, CompilationOptions rebuildOptions)
{
var type = originalOptions.GetType();
foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
switch (propertyInfo.Name)
{
case nameof(CompilationOptions.GeneralDiagnosticOption):
case nameof(CompilationOptions.Features):
case nameof(CompilationOptions.ModuleName):
case nameof(CompilationOptions.MainTypeName):
case nameof(CompilationOptions.ConcurrentBuild):
case nameof(CompilationOptions.WarningLevel):
case nameof(CompilationOptions.SyntaxTreeOptionsProvider):
case nameof(CompilationOptions.MetadataReferenceResolver):
case nameof(CompilationOptions.XmlReferenceResolver):
case nameof(CompilationOptions.SourceReferenceResolver):
case nameof(CompilationOptions.StrongNameProvider):
case nameof(CompilationOptions.AssemblyIdentityComparer):
// Can be different and are special cased
break;
case nameof(VisualBasicCompilationOptions.ParseOptions):
{
var originalValue = propertyInfo.GetValue(originalOptions)!;
var rebuildValue = propertyInfo.GetValue(rebuildOptions)!;
VerifyParseOptions((ParseOptions)originalValue, (ParseOptions)rebuildValue);
}
break;
default:
{
var originalValue = propertyInfo.GetValue(originalOptions);
var rebuildValue = propertyInfo.GetValue(rebuildOptions);
Assert.Equal(originalValue, rebuildValue);
}
break;
}
}
}
#pragma warning restore 612
private static void VerifyParseOptions(ParseOptions originalOptions, ParseOptions rebuildOptions)
{
var type = originalOptions.GetType();
foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
// Several options are expected to be different and they are special cased here.
if (propertyInfo.Name == nameof(VisualBasicParseOptions.SpecifiedLanguageVersion))
{
continue;
}
var originalValue = propertyInfo.GetValue(originalOptions);
var rebuildValue = propertyInfo.GetValue(rebuildOptions);
Assert.Equal(originalValue, rebuildValue);
}
}
}
}
|