File: CompilationVerifier.cs
Web Access
Project: src\src\Compilers\Test\Core\Microsoft.CodeAnalysis.Test.Utilities.csproj (Microsoft.CodeAnalysis.Test.Utilities)
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml.Linq;
using ICSharpCode.Decompiler.Metadata;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeGen;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.DiaSymReader.Tools;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Test.Utilities
{
    public sealed partial class CompilationVerifier
    {
        /// <summary>
        /// When true this will dump assemblies to disk when verification fails or there are emit errors writing
        /// the compilation to bytes
        /// </summary>
        internal static bool DumpAssembliesOnFailure { get; set; }
 
        private static int s_dumpCount;
 
        private readonly Compilation _compilation;
        private readonly IEnumerable<ModuleData>? _dependencies;
        private IModuleSymbol? _lazyModuleSymbol;
        private EmitData? _emitData;
        private readonly Func<IModuleSymbol, CompilationTestData.MethodData, IReadOnlyDictionary<int, string>?, bool, string>? _visualizeRealIL;
 
        public Compilation Compilation => _compilation;
        public ImmutableArray<byte> EmittedAssemblyData => GetEmitData().EmittedAssemblyData;
        public ImmutableArray<byte> EmittedAssemblyPdb => GetEmitData().EmittedAssemblyPdb;
        public ImmutableArray<Diagnostic> Diagnostics => GetEmitData().Diagnostics;
        internal CompilationTestData TestData => GetEmitData().TestData;
 
        internal CompilationVerifier(
            Compilation compilation,
            Func<IModuleSymbol, CompilationTestData.MethodData, IReadOnlyDictionary<int, string>?, bool, string>? visualizeRealIL = null,
            IEnumerable<ModuleData>? dependencies = null)
        {
            _compilation = compilation;
            _dependencies = dependencies;
            _visualizeRealIL = visualizeRealIL;
        }
 
        private EmitData GetEmitData() => _emitData ?? throw new InvalidOperationException("Must call Emit first");
 
        internal Metadata GetMetadata()
        {
            var emitData = GetEmitData();
            if (_compilation.Options.OutputKind.IsNetModule())
            {
                var metadata = ModuleMetadata.CreateFromImage(emitData.EmittedAssemblyData);
                metadata.Module.PretendThereArentNoPiaLocalTypes();
                return metadata;
            }
            else
            {
                List<ImmutableArray<byte>> images =
                [
                    emitData.EmittedAssemblyData,
                    .. emitData.Modules.Where(m => m.Kind == OutputKind.NetModule).Select(m => m.Image)
                ];
 
                return AssemblyMetadata.Create(images.Select(image =>
                {
                    var metadata = ModuleMetadata.CreateFromImage(image);
                    metadata.Module.PretendThereArentNoPiaLocalTypes();
                    return metadata;
                }));
            }
        }
 
        public string Dump(string? methodName = null)
        {
            var emitData = Emit(manifestResources: null, EmitOptions.Default);
            var dumpDir = DumpAssemblyData(emitData.Modules);
            string extension = emitData.EmittedModule.Kind == OutputKind.ConsoleApplication ? ".exe" : ".dll";
            string modulePath = Path.Combine(dumpDir, emitData.EmittedModule.SimpleName + extension);
 
            var decompiler = new ICSharpCode.Decompiler.CSharp.CSharpDecompiler(modulePath,
                new ICSharpCode.Decompiler.DecompilerSettings() { AsyncAwait = false });
 
            if (methodName != null)
            {
                var map = new Dictionary<string, ICSharpCode.Decompiler.TypeSystem.IMethod>();
                listMethods(decompiler.TypeSystem.MainModule.RootNamespace, map);
 
                if (map.TryGetValue(methodName, out var method))
                {
                    return decompiler.DecompileAsString(method.MetadataToken);
                }
                else
                {
                    throw new Exception($"Didn't find method '{methodName}'. Available/distinguishable methods are: {Environment.NewLine}{string.Join(Environment.NewLine, map.Keys)}");
                }
            }
 
            return decompiler.DecompileWholeModuleAsString();
 
            void listMethods(ICSharpCode.Decompiler.TypeSystem.INamespace @namespace, Dictionary<string, ICSharpCode.Decompiler.TypeSystem.IMethod> result)
            {
                foreach (var nestedNS in @namespace.ChildNamespaces)
                {
                    if (nestedNS.FullName != "System" &&
                        nestedNS.FullName != "Microsoft")
                    {
                        listMethods(nestedNS, result);
                    }
                }
 
                foreach (var type in @namespace.Types)
                {
                    listMethodsInType(type, result);
                }
            }
 
            void listMethodsInType(ICSharpCode.Decompiler.TypeSystem.ITypeDefinition type, Dictionary<string, ICSharpCode.Decompiler.TypeSystem.IMethod> result)
            {
                foreach (var nestedType in type.NestedTypes)
                {
                    listMethodsInType(nestedType, result);
                }
 
                foreach (var method in type.Methods)
                {
                    if (result.ContainsKey(method.FullName))
                    {
                        // There is a bug with FullName on methods in generic types
                        result.Remove(method.FullName);
                    }
                    else
                    {
                        result.Add(method.FullName, method);
                    }
                }
            }
        }
 
        public string DumpIL()
        {
            var output = new ICSharpCode.Decompiler.PlainTextOutput();
            var emitData = Emit(manifestResources: null, EmitOptions.Default);
            using var moduleMetadata = ModuleMetadata.CreateFromImage(emitData.EmittedAssemblyData);
            var peFile = new PEFile(emitData.EmittedModule.Id.FullName, moduleMetadata.Module.PEReaderOpt);
            var disassembler = new ICSharpCode.Decompiler.Disassembler.ReflectionDisassembler(output, default);
            disassembler.WriteModuleContents(peFile);
            return output.ToString();
        }
 
        public static string DumpAssemblyData(IEnumerable<ModuleData> modules)
        {
            var dumpCount = Interlocked.Increment(ref s_dumpCount);
            var dumpDirectory = Path.Combine(TempRoot.Root, "dumps", dumpCount.ToString());
            _ = Directory.CreateDirectory(dumpDirectory);
 
            // Limit the number of dumps to 10. After 10 we're likely in a bad state and are 
            // dumping lots of unnecessary data to disk.
            if (dumpCount > 10)
            {
                return dumpDirectory;
            }
 
            var sb = new StringBuilder();
            foreach (var module in modules)
            {
                if (module.InMemoryModule)
                {
                    string fileName;
                    if (module.Kind == OutputKind.NetModule)
                    {
                        fileName = module.FullName;
                    }
                    else
                    {
                        fileName = AssemblyIdentity.TryParseDisplayName(module.FullName, out var identity)
                            ? identity.Name
                            : "";
                    }
 
                    string pePath = Path.Combine(dumpDirectory, fileName + module.Kind.GetDefaultExtension());
                    try
                    {
                        module.Image.WriteToFile(pePath);
                    }
                    catch (ArgumentException e)
                    {
                        pePath = $"<unable to write file: '{pePath}' -- {e.Message}>";
                    }
                    catch (IOException e)
                    {
                        pePath = $"<unable to write file: '{pePath}' -- {e.Message}>";
                    }
 
                    string? pdbPath;
                    if (!module.Pdb.IsDefaultOrEmpty)
                    {
                        pdbPath = Path.Combine(dumpDirectory, fileName + ".pdb");
 
                        try
                        {
                            module.Pdb.WriteToFile(pdbPath);
                        }
                        catch (ArgumentException e)
                        {
                            pdbPath = $"<unable to write file: '{pdbPath}' -- {e.Message}>";
                        }
                        catch (IOException e)
                        {
                            pdbPath = $"<unable to write file: '{pdbPath}' -- {e.Message}>";
                        }
                    }
                    else
                    {
                        pdbPath = null;
                    }
 
                    sb.Append("PE(" + module.Kind + "): ");
                    sb.AppendLine(pePath);
                    if (pdbPath != null)
                    {
                        sb.Append("PDB: ");
                        sb.AppendLine(pdbPath);
                    }
                }
            }
 
            if (sb.Length > 0)
            {
                File.WriteAllText(Path.Combine(dumpDirectory, "log.txt"), sb.ToString());
            }
 
            return dumpDirectory;
        }
 
        /// <summary>
        /// Asserts that the emitted IL for a type is the same as the expected IL.
        /// Many core library types are in different assemblies on .Net Framework, and .Net Core.
        /// Therefore this test is likely to fail unless you  only run it only only on one of these frameworks,
        /// or you run it on both, but provide a different expected output string for each.
        /// See <see cref="ExecutionConditionUtil"/>.
        /// </summary>
        /// <param name="typeName">The non-fully-qualified name of the type</param>
        /// <param name="expected">The expected IL</param>
        public void VerifyTypeIL(string typeName, string expected)
        {
            VerifyTypeIL(typeName, output =>
            {
                // All our tests predate ilspy adding `// Header size: ...` to the contents.  So trim that out since we
                // really don't need to validate superfluous IL comments
                expected = RemoveHeaderComments(expected);
                output = RemoveHeaderComments(output);
 
                output = FixupCodeSizeComments(output);
 
                AssertEx.AssertEqualToleratingWhitespaceDifferences(expected, output, escapeQuotes: false);
            });
        }
 
        private static readonly Regex s_headerCommentsRegex = new("""^\s*// Header size: [0-9]+\s*$""", RegexOptions.Multiline);
        private static readonly Regex s_codeSizeCommentsRegex = new("""^\s*// Code size(:) [0-9]+\s*""", RegexOptions.Multiline);
 
        private static string RemoveHeaderComments(string value)
        {
            return s_headerCommentsRegex.Replace(value, "");
        }
 
        private static string FixupCodeSizeComments(string output)
        {
            // We use the form `// Code size 7 (0x7)` while ilspy moved to the form `// Code size: 7 (0x7)` (with an
            // extra colon).  Strip the colon to make these match.
            return s_codeSizeCommentsRegex.Replace(output, match => match.Groups[0].Value.Replace(match.Groups[1].Value, ""));
        }
 
        /// <summary>
        /// Invokes <paramref name="validateExpected"/> with the emitted IL for a type to validate it's expected.
        /// Many core library types are in different assemblies on .Net Framework, and .Net Core.
        /// Therefore this test is likely to fail unless you  only run it only only on one of these frameworks,
        /// or you run it on both, but provide a different expected output string for each.
        /// See <see cref="ExecutionConditionUtil"/>.
        /// </summary>
        /// <param name="typeName">The non-fully-qualified name of the type</param>
        /// <param name="validateExpected">An action to invoke with the emitted IL.</param>
        public void VerifyTypeIL(string typeName, Action<string> validateExpected)
        {
            var output = new ICSharpCode.Decompiler.PlainTextOutput() { IndentationString = "    " };
            var emitData = Emit(manifestResources: null, EmitOptions.Default);
            using (var moduleMetadata = ModuleMetadata.CreateFromImage(emitData.EmittedAssemblyData))
            {
                var peFile = new PEFile(emitData.EmittedModule.Id.FullName, moduleMetadata.Module.PEReaderOpt);
                var metadataReader = moduleMetadata.GetMetadataReader();
 
                bool found = false;
                foreach (var typeDefHandle in metadataReader.TypeDefinitions)
                {
                    var typeDef = metadataReader.GetTypeDefinition(typeDefHandle);
                    if (metadataReader.GetString(typeDef.Name) == typeName)
                    {
                        var disassembler = new ICSharpCode.Decompiler.Disassembler.ReflectionDisassembler(output, default);
                        disassembler.DisassembleType(peFile, typeDefHandle);
                        found = true;
                        break;
                    }
                }
                Assert.True(found, "Could not find type named " + typeName);
            }
 
            validateExpected(output.ToString());
        }
 
        public void EmitAndVerify(
            string? expectedOutput,
            bool trimOutput,
            int? expectedReturnCode,
            string[]? args,
            IEnumerable<ResourceDescription>? manifestResources,
            EmitOptions? emitOptions,
            Verification peVerify,
            SignatureDescription[]? expectedSignatures)
        {
 
            var emitData = Emit(manifestResources, emitOptions);
            using var testEnvironment = CreateRuntimeEnvironment(emitData.EmittedModule, emitData.Modules);
 
            try
            {
                testEnvironment.Verify(peVerify);
            }
            catch (Exception)
            {
                if (DumpAssembliesOnFailure)
                {
                    DumpAssemblyData(emitData.Modules);
                }
 
                if (peVerify.Status.HasFlag(VerificationStatus.PassesOrFailFast))
                {
                    var il = DumpIL();
                    Console.WriteLine(il);
 
                    Environment.FailFast("Investigating flaky IL verification issue. Tracked by https://github.com/dotnet/roslyn/issues/63782");
                }
 
                throw;
            }
 
            if (expectedSignatures != null)
            {
                MetadataSignatureUnitTestHelper.VerifyMemberSignatures(testEnvironment, expectedSignatures);
            }
 
            if (expectedOutput != null || expectedReturnCode != null)
            {
                var (exitCode, output, errorOutput) = testEnvironment.Execute(args ?? []);
                if (expectedReturnCode.HasValue)
                {
                    Assert.Equal(expectedReturnCode.Value, exitCode);
                }
 
                if (expectedOutput != null)
                {
                    if (trimOutput)
                    {
                        expectedOutput = expectedOutput.Trim();
                        output = output.Trim();
                    }
 
                    Assert.Equal(expectedOutput, output);
                    Assert.Empty(errorOutput);
                }
            }
        }
 
        private sealed class Resolver : ILVerify.IResolver
        {
            private readonly Dictionary<string, PEReader> _readersByName;
 
            internal Resolver(Dictionary<string, PEReader> readersByName)
            {
                _readersByName = readersByName;
            }
 
            public PEReader ResolveAssembly(AssemblyName assemblyName)
            {
                Debug.Assert(assemblyName.Name is not null);
                return Resolve(assemblyName.Name);
            }
 
            public PEReader ResolveModule(AssemblyName referencingAssembly, string fileName)
            {
                throw new NotImplementedException();
            }
 
            public PEReader Resolve(string simpleName)
            {
                if (_readersByName.TryGetValue(simpleName, out var reader))
                {
                    return reader;
                }
 
                throw new Exception($"ILVerify was not able to resolve a module named '{simpleName}'");
            }
        }
 
        internal static void ILVerify(Verification verification, ModuleData mainModule, ImmutableArray<ModuleData> modules)
        {
            if (verification.Status.HasFlag(VerificationStatus.Skipped))
            {
                return;
            }
 
            var readersByName = new Dictionary<string, PEReader>(StringComparer.OrdinalIgnoreCase);
            foreach (var module in modules)
            {
                string name = module.SimpleName;
                if (readersByName.ContainsKey(name))
                {
                    if (verification.Status.HasFlag(VerificationStatus.FailsILVerify) && verification.ILVerifyMessage is null)
                    {
                        return;
                    }
 
                    throw new Exception($"Multiple modules named '{name}' were found");
                }
                readersByName.Add(name, new PEReader(module.Image));
            }
 
            var resolver = new Resolver(readersByName);
            var verifier = new ILVerify.Verifier(resolver);
            var mscorlibModule = modules.SingleOrDefault(m => m.IsCorLib);
            if (mscorlibModule is null)
            {
                if (verification.Status.HasFlag(VerificationStatus.FailsILVerify) && verification.ILVerifyMessage is null)
                {
                    return;
                }
 
                throw new Exception("No corlib found");
            }
 
            // Main module is the first one
            var mainModuleReader = resolver.Resolve(mainModule.SimpleName);
 
            var (actualSuccess, actualMessage) = verify(verifier, mscorlibModule.FullName, mainModuleReader);
            var expectedSuccess = !verification.Status.HasFlag(VerificationStatus.FailsILVerify);
 
            if (actualSuccess != expectedSuccess)
            {
                throw new Exception(expectedSuccess ?
                    $"IL Verify failed unexpectedly:{Environment.NewLine}{actualMessage}" :
                    "IL Verify succeeded unexpectedly");
            }
 
            if (!actualSuccess && verification.ILVerifyMessage != null && !IsEnglishLocal.Instance.ShouldSkip)
            {
                if (!verification.IncludeTokensAndModuleIds)
                {
                    actualMessage = Regex.Replace(actualMessage, @"\[[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\]", "");
                }
 
                AssertEx.AssertEqualToleratingWhitespaceDifferences(verification.ILVerifyMessage, actualMessage);
            }
 
            return;
 
            static (bool, string) verify(ILVerify.Verifier verifier, string corlibName, PEReader mainModule)
            {
                IEnumerable<ILVerify.VerificationResult>? result = null;
                int errorCount = 0;
                try
                {
                    verifier.SetSystemModuleName(new AssemblyName(corlibName));
                    result = verifier.Verify(mainModule);
                    errorCount = result.Count();
                }
                catch (Exception e)
                {
                    return (false, e.Message);
                }
 
                if (errorCount > 0)
                {
                    var metadataReader = mainModule.GetMetadataReader();
                    return (false, printVerificationResult(result, metadataReader));
                }
 
                return (true, string.Empty);
            }
 
            static string printVerificationResult(IEnumerable<ILVerify.VerificationResult> result, MetadataReader metadataReader)
            {
                return string.Join(Environment.NewLine, result.Select(r => printMethod(r.Method, metadataReader) + r.Message + printErrorArguments(r.ErrorArguments)));
            }
 
            static string printMethod(MethodDefinitionHandle method, MetadataReader metadataReader)
            {
                if (method.IsNil)
                {
                    return "";
                }
 
                var methodName = metadataReader.GetString(metadataReader.GetMethodDefinition(method).Name);
                return $"[{methodName}]: ";
            }
 
            static string printErrorArguments(ILVerify.ErrorArgument[] errorArguments)
            {
                if (errorArguments is null
                    || errorArguments.Length == 0)
                {
                    return "";
                }
 
                var pooledBuilder = PooledStringBuilder.GetInstance();
                var builder = pooledBuilder.Builder;
                builder.Append(" { ");
                var x = errorArguments.Select(a => printErrorArgument(a)).ToArray();
                for (int i = 0; i < x.Length; i++)
                {
                    if (i > 0)
                    {
                        builder.Append(", ");
                    }
                    builder.Append(x[i]);
                }
                builder.Append(" }");
 
                return pooledBuilder.ToStringAndFree();
            }
 
            static string printErrorArgument(ILVerify.ErrorArgument errorArgument)
            {
                var name = errorArgument.Name;
 
                string? value;
                if (name == "Offset" && errorArgument.Value is int i)
                {
                    value = "0x" + Convert.ToString(i, 16);
                }
                else
                {
                    Debug.Assert(errorArgument.Value != null);
                    value = errorArgument.Value.ToString();
                }
 
                return name + " = " + value;
            }
        }
 
        // TODO(tomat): Fold into CompileAndVerify. 
        // Replace bool verify parameter with string[] expectedPeVerifyOutput. If null, no verification. If empty verify have to succeed. Otherwise compare errors.
        public void EmitAndVerify(params string[] expectedPeVerifyOutput)
        {
            var emitData = Emit(null, null);
            using var testEnvironment = CreateRuntimeEnvironment(emitData.EmittedModule, emitData.Modules);
            string[] actualOutput = testEnvironment.VerifyModules([emitData.EmittedModule.FullName]);
            Assert.Equal(expectedPeVerifyOutput, actualOutput);
        }
 
        private EmitData Emit(IEnumerable<ResourceDescription>? manifestResources, EmitOptions? emitOptions)
        {
            var testData = new CompilationTestData();
            var diagnostics = DiagnosticBag.GetInstance();
            var dependencyList = new List<ModuleData>();
            var emitOutput = EmitCompilation(
                _compilation,
                manifestResources,
                dependencyList,
                diagnostics,
                testData,
                emitOptions);
 
            if (emitOutput is { } e)
            {
                var corLibIdentity = _compilation.GetSpecialType(SpecialType.System_Object).ContainingAssembly.Identity;
                var identity = _compilation.Assembly.Identity;
                var moduleData = new ModuleData(
                    identity,
                    _compilation.Options.OutputKind,
                    e.Assembly,
                    pdb: e.Pdb,
                    inMemoryModule: true,
                    isCorLib: corLibIdentity == identity);
 
                // We need to add the main module so that it gets checked against already loaded assembly names.
                // If an assembly is loaded directly via PEVerify(image) another assembly of the same full name
                // can't be loaded as a dependency (via Assembly.ReflectionOnlyLoad) in the same domain.
                dependencyList.Insert(0, moduleData);
 
                _emitData = new EmitData(
                    moduleData,
                    dependencyList.ToImmutableArray(),
                    diagnostics.ToReadOnlyAndFree(),
                    testData);
                return _emitData;
            }
            else
            {
                var dumpDir = DumpAssembliesOnFailure ? DumpAssemblyData(dependencyList) : null;
                throw new EmitException(diagnostics.ToReadOnlyAndFree(), dumpDir);
            }
        }
 
        private IRuntimeEnvironment CreateRuntimeEnvironment(ModuleData mainModule, ImmutableArray<ModuleData> modules)
        {
            if (_dependencies is not null)
            {
                modules = [.. modules, .. _dependencies];
            }
 
            return RuntimeUtilities.CreateRuntimeEnvironment(mainModule, modules);
        }
 
        /// <summary>
        /// Obsolete. Use <see cref="VerifyMethodBody(string, string, bool, string, int, SymbolDisplayFormat?)"/> instead.
        /// </summary>
        public CompilationVerifier VerifyIL(
            string qualifiedMethodName,
            XCData expectedIL,
            bool realIL = false,
            string? sequencePoints = null,
            [CallerFilePath] string? callerPath = null,
            [CallerLineNumber] int callerLine = 0)
        {
            return VerifyILImpl(qualifiedMethodName, expectedIL.Value, realIL, sequencePoints: sequencePoints != null, sequencePointsSource: false, callerPath, callerLine, escapeQuotes: false, ilFormat: null);
        }
 
        /// <summary>
        /// Obsolete. Use <see cref="VerifyMethodBody(string, string, bool, string, int, SymbolDisplayFormat?)"/> instead.
        /// </summary>
        public CompilationVerifier VerifyIL(
            string qualifiedMethodName,
            string expectedIL,
            bool realIL = false,
            string? sequencePoints = null,
            [CallerFilePath] string? callerPath = null,
            [CallerLineNumber] int callerLine = 0,
            string? source = null,
            SymbolDisplayFormat? ilFormat = null)
        {
            return VerifyILImpl(qualifiedMethodName, expectedIL, realIL, sequencePoints: sequencePoints != null, sequencePointsSource: source != null, callerPath, callerLine, escapeQuotes: false, ilFormat);
        }
 
        public CompilationVerifier VerifyMethodBody(
            string qualifiedMethodName,
            string expectedILWithSequencePoints,
            bool realIL = false,
            [CallerFilePath] string? callerPath = null,
            [CallerLineNumber] int callerLine = 0,
            SymbolDisplayFormat? ilFormat = null)
        {
            return VerifyILImpl(qualifiedMethodName, expectedILWithSequencePoints, realIL, sequencePoints: true, sequencePointsSource: true, callerPath, callerLine, escapeQuotes: false, ilFormat);
        }
 
        public void VerifyILMultiple(params string[] qualifiedMethodNamesAndExpectedIL)
        {
            var names = ArrayBuilder<string>.GetInstance();
            var expected = ArrayBuilder<string>.GetInstance();
            var actual = ArrayBuilder<string>.GetInstance();
            for (int i = 0; i < qualifiedMethodNamesAndExpectedIL.Length;)
            {
                var qualifiedName = qualifiedMethodNamesAndExpectedIL[i++];
                names.Add(qualifiedName);
                actual.Add(AssertEx.NormalizeWhitespace(VisualizeIL(qualifiedName)));
                expected.Add(AssertEx.NormalizeWhitespace(qualifiedMethodNamesAndExpectedIL[i++]));
            }
            if (!expected.SequenceEqual(actual))
            {
                var builder = new StringBuilder();
                for (int i = 0; i < expected.Count; i++)
                {
                    builder.AppendLine(AssertEx.GetAssertMessage(expected[i], actual[i], prefix: names[i], escapeQuotes: true));
                }
                Assert.True(false, builder.ToString());
            }
            actual.Free();
            expected.Free();
            names.Free();
        }
 
        public CompilationVerifier VerifyMissing(
            string qualifiedMethodName)
        {
            Assert.False(GetEmitData().TestData.TryGetMethodData(qualifiedMethodName, out _));
            return this;
        }
 
        public void VerifyLocalSignature(
            string qualifiedMethodName,
            string expectedSignature,
            [CallerLineNumber] int callerLine = 0,
            [CallerFilePath] string? callerPath = null)
        {
            var ilBuilder = GetEmitData().TestData.GetMethodData(qualifiedMethodName).ILBuilder;
            string actualSignature = ILBuilderVisualizer.LocalSignatureToString(ilBuilder);
            AssertEx.AssertEqualToleratingWhitespaceDifferences(expectedSignature, actualSignature, escapeQuotes: true, expectedValueSourcePath: callerPath, expectedValueSourceLine: callerLine);
        }
 
        /// <summary>
        /// Visualizes the IL for a given method, and ensures that it matches the expected IL.
        /// </summary>
        /// <param name="realIL">Controls whether the IL stream contains pseudo-tokens or real tokens.</param>
        private CompilationVerifier VerifyILImpl(
            string qualifiedMethodName,
            string expectedIL,
            bool realIL,
            bool sequencePoints,
            bool sequencePointsSource,
            string? callerPath,
            int callerLine,
            bool escapeQuotes,
            SymbolDisplayFormat? ilFormat)
        {
            string? actualIL = VisualizeIL(qualifiedMethodName, realIL, sequencePoints, sequencePointsSource, ilFormat);
            AssertEx.AssertEqualToleratingWhitespaceDifferences(expectedIL, actualIL, message: null, escapeQuotes, callerPath, callerLine);
            return this;
        }
 
        public string VisualizeIL(string qualifiedMethodName, bool realIL = false, bool sequencePoints = false, bool sequencePointsSource = true, SymbolDisplayFormat? ilFormat = null)
            => VisualizeIL(GetEmitData().TestData.GetMethodData(qualifiedMethodName), realIL, sequencePoints, sequencePointsSource, ilFormat);
 
        internal string VisualizeIL(CompilationTestData.MethodData methodData, bool realIL = false, bool sequencePoints = false, bool sequencePointsSource = true, SymbolDisplayFormat? ilFormat = null)
        {
            Dictionary<int, string>? markers = null;
 
            var emitData = GetEmitData();
            if (sequencePoints)
            {
                var actualPdbXml = PdbToXmlConverter.ToXml(
                    pdbStream: new MemoryStream(emitData.EmittedAssemblyPdb.ToArray()),
                    peStream: new MemoryStream(emitData.EmittedAssemblyData.ToArray()),
                    options: PdbToXmlOptions.ResolveTokens |
                             PdbToXmlOptions.ThrowOnError |
                             PdbToXmlOptions.ExcludeCustomDebugInformation |
                             PdbToXmlOptions.ExcludeScopes |
                             PdbToXmlOptions.IncludeTokens);
 
                if (actualPdbXml.StartsWith("<error>"))
                {
                    throw new Exception($"Failed to extract PDB information. PdbToXmlConverter returned:{Environment.NewLine}{actualPdbXml}");
                }
 
                var method = methodData.Method.PartialDefinitionPart ?? methodData.Method;
                var methodDef = (Cci.IMethodDefinition)method.GetCciAdapter();
                var methodToken = MetadataTokens.GetToken(emitData.TestData.MetadataWriter!.GetMethodDefinitionOrReferenceHandle(methodDef));
                var xmlDocument = XElement.Parse(actualPdbXml);
                var xmlMethod = ILValidation.GetMethodElement(xmlDocument, methodToken);
 
                // method may not have any debug info and thus no sequence points
                if (xmlMethod != null)
                {
                    var documentMap = ILValidation.GetDocumentIdToPathMap(xmlDocument);
 
                    markers = sequencePointsSource ?
                        ILValidation.GetSequencePointMarkers(xmlMethod, id => _compilation.SyntaxTrees.Single(tree => tree.FilePath == documentMap[id]).GetText()) :
                        ILValidation.GetSequencePointMarkers(xmlMethod);
                }
            }
 
            if (!realIL)
            {
                return ILBuilderVisualizer.ILBuilderToString(methodData.ILBuilder, markers: markers, ilFormat: ilFormat);
            }
 
            if (_lazyModuleSymbol == null)
            {
                var targetReference = LoadTestEmittedExecutableForSymbolValidation(emitData.EmittedAssemblyData, _compilation.Options.OutputKind, display: _compilation.AssemblyName);
                _lazyModuleSymbol = GetSymbolFromMetadata(targetReference, MetadataImportOptions.All);
            }
 
            if (_lazyModuleSymbol != null)
            {
                if (_visualizeRealIL == null)
                {
                    throw new InvalidOperationException("IL visualization function is not set");
                }
 
                return _visualizeRealIL(_lazyModuleSymbol, methodData, markers, emitData.TestData.Module!.GetMethodBody(methodData.Method)!.AreLocalsZeroed);
            }
 
            return "";
        }
 
        public CompilationVerifier VerifyMemberInIL(string methodName, bool expected)
        {
            Assert.Equal(expected, GetEmitData().TestData.GetMethodsByName().ContainsKey(methodName));
            return this;
        }
 
        public CompilationVerifier VerifyDiagnostics(params DiagnosticDescription[] expected)
        {
            GetEmitData().Diagnostics.Verify(expected);
            return this;
        }
 
        internal IModuleSymbol GetSymbolFromMetadata(MetadataReference metadataReference, MetadataImportOptions importOptions)
        {
            var dummy = _compilation
                .RemoveAllSyntaxTrees()
                .AddReferences(metadataReference)
                .WithAssemblyName("Dummy")
                .WithOptions(_compilation.Options.WithMetadataImportOptions(importOptions));
 
            var symbol = dummy.GetAssemblyOrModuleSymbol(metadataReference);
            Debug.Assert(symbol is not null);
 
            if (metadataReference.Properties.Kind == MetadataImageKind.Assembly)
            {
                return ((IAssemblySymbol)symbol).Modules.First();
            }
            else
            {
                return (IModuleSymbol)symbol;
            }
        }
 
        internal static MetadataReference LoadTestEmittedExecutableForSymbolValidation(
            ImmutableArray<byte> image,
            OutputKind outputKind,
            string? display = null)
        {
            var moduleMetadata = ModuleMetadata.CreateFromImage(image);
            moduleMetadata.Module.PretendThereArentNoPiaLocalTypes();
 
            if (outputKind == OutputKind.NetModule)
            {
                return moduleMetadata.GetReference(display: display);
            }
            else
            {
                return AssemblyMetadata.Create(moduleMetadata).GetReference(display: display);
            }
        }
 
        public void VerifyOperationTree(string expectedOperationTree, bool skipImplicitlyDeclaredSymbols = false)
        {
            _compilation.VerifyOperationTree(expectedOperationTree, skipImplicitlyDeclaredSymbols);
        }
 
        public void VerifyOperationTree(string symbolToVerify, string expectedOperationTree, bool skipImplicitlyDeclaredSymbols = false)
        {
            _compilation.VerifyOperationTree(symbolToVerify, expectedOperationTree, skipImplicitlyDeclaredSymbols);
        }
 
        /// <summary>
        /// Useful for verifying the expected variables are hoisted for closures, async, and iterator methods.
        /// </summary>
        public void VerifySynthesizedFields(string containingTypeName, params string[] expectedFields)
        {
            var testData = GetEmitData().TestData;
            var types = testData.Module!.GetAllSynthesizedMembers();
            Assert.Contains(types.Keys, t => containingTypeName == t.ToString());
            var members = testData.Module.GetAllSynthesizedMembers()
                .Where(e => e.Key.ToString() == containingTypeName)
                .Single()
                .Value
                .Where(s => s.Kind == SymbolKind.Field)
                .Select(f => $"{((IFieldSymbol)f.GetISymbol()).Type.ToString()} {f.Name}")
                .ToList();
            AssertEx.SetEqual(expectedFields, members);
        }
 
        /// <summary>
        /// Emit all of the references which are not directly or indirectly a <see cref="Compilation"/> value.
        /// </summary>
        internal static void EmitReferences(Compilation compilation, HashSet<string> fullNameSet, List<ModuleData> dependencies, AssemblyIdentity corLibIdentity)
        {
            // NOTE: specifically don't need to consider previous submissions since they will always be compilations.
            foreach (var metadataReference in compilation.References)
            {
                if (metadataReference is CompilationReference)
                {
                    continue;
                }
 
                var peRef = (PortableExecutableReference)metadataReference;
                var metadata = peRef.GetMetadataNoCopy();
                var isManifestModule = peRef.Properties.Kind == MetadataImageKind.Assembly;
                var identity = isManifestModule
                    ? ((AssemblyMetadata)metadata).GetAssembly()!.Identity
                    : null;
 
                // If this is an indirect reference to a Compilation then it is already been emitted 
                // so no more work to be done.
                if (isManifestModule && fullNameSet.Contains(identity!.GetDisplayName()))
                {
                    continue;
                }
 
                var isCorLib = isManifestModule && corLibIdentity == identity;
                foreach (var module in enumerateModules(metadata))
                {
                    ImmutableArray<byte> bytes = module.Module.PEReaderOpt.GetEntireImage().GetContent();
                    ModuleData moduleData;
                    if (isManifestModule)
                    {
                        fullNameSet.Add(identity!.GetDisplayName());
                        moduleData = new ModuleData(identity,
                                                    OutputKind.DynamicallyLinkedLibrary,
                                                    bytes,
                                                    pdb: default(ImmutableArray<byte>),
                                                    inMemoryModule: true,
                                                    isCorLib);
                    }
                    else
                    {
                        moduleData = new ModuleData(module.Name,
                                                    bytes,
                                                    pdb: default(ImmutableArray<byte>),
                                                    inMemoryModule: true,
                                                    isCorLib: false);
                    }
 
                    dependencies.Add(moduleData);
                    isManifestModule = false;
                }
            }
 
            static IEnumerable<ModuleMetadata> enumerateModules(Metadata metadata)
            {
                return (metadata.Kind == MetadataImageKind.Assembly) ? ((AssemblyMetadata)metadata).GetModules().AsEnumerable() : SpecializedCollections.SingletonEnumerable((ModuleMetadata)metadata);
            }
        }
 
        internal static EmitOutput? EmitCompilation(
            Compilation compilation,
            IEnumerable<ResourceDescription>? manifestResources,
            List<ModuleData> dependencies,
            DiagnosticBag diagnostics,
            CompilationTestData? testData,
            EmitOptions? emitOptions)
        {
            var corLibIdentity = compilation.GetSpecialType(SpecialType.System_Object).ContainingAssembly.Identity;
 
            // A Compilation can appear multiple times in a dependency graph as both a Compilation and as a MetadataReference
            // value.  Iterate the Compilations eagerly so they are always emitted directly and later references can re-use 
            // the value.  This gives better, and consistent, diagnostic information.
            var referencedCompilations = findReferencedCompilations(compilation);
            var fullNameSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
            foreach (var referencedCompilation in referencedCompilations)
            {
                var emitData = emitCompilationCore(referencedCompilation, null, diagnostics, null, emitOptions);
                if (emitData.HasValue)
                {
                    var identity = referencedCompilation.Assembly.Identity;
                    var moduleData = new ModuleData(identity,
                                                    OutputKind.DynamicallyLinkedLibrary,
                                                    emitData.Value.Assembly,
                                                    pdb: default(ImmutableArray<byte>),
                                                    inMemoryModule: true,
                                                    isCorLib: corLibIdentity == identity);
                    fullNameSet.Add(moduleData.Id.FullName);
                    dependencies.Add(moduleData);
                }
            }
 
            // Now that the Compilation values have been emitted, emit the non-compilation references
            foreach (var current in (new[] { compilation }).Concat(referencedCompilations))
            {
                EmitReferences(current, fullNameSet, dependencies, corLibIdentity);
            }
 
            return emitCompilationCore(compilation, manifestResources, diagnostics, testData, emitOptions);
 
            // Find all of the <see cref="Compilation"/> values reachable from this instance.
            static List<Compilation> findReferencedCompilations(Compilation original)
            {
                var list = new List<Compilation>();
                var toVisit = new Queue<Compilation>(findDirectReferencedCompilations(original));
 
                while (toVisit.Count > 0)
                {
                    var current = toVisit.Dequeue();
                    if (list.Contains(current))
                    {
                        continue;
                    }
 
                    list.Add(current);
 
                    foreach (var other in findDirectReferencedCompilations(current))
                    {
                        toVisit.Enqueue(other);
                    }
                }
 
                return list;
            }
 
            static List<Compilation> findDirectReferencedCompilations(Compilation compilation)
            {
                var list = new List<Compilation>();
                var previousCompilation = compilation.ScriptCompilationInfo?.PreviousScriptCompilation;
                if (previousCompilation != null)
                {
                    list.Add(previousCompilation);
                }
 
                foreach (var reference in compilation.References.OfType<CompilationReference>())
                {
                    list.Add(reference.Compilation);
                }
 
                return list;
            }
 
            static EmitOutput? emitCompilationCore(
                Compilation compilation,
                IEnumerable<ResourceDescription>? manifestResources,
                DiagnosticBag diagnostics,
                CompilationTestData? testData,
                EmitOptions? emitOptions)
            {
                emitOptions ??= EmitOptions.Default.WithDebugInformationFormat(DebugInformationFormat.Embedded);
 
                using var executableStream = new MemoryStream();
 
                var pdb = default(ImmutableArray<byte>);
                var assembly = default(ImmutableArray<byte>);
                var pdbStream = (emitOptions.DebugInformationFormat != DebugInformationFormat.Embedded) ? new MemoryStream() : null;
 
                // Note: don't forget to name the source inputs to get them embedded for debugging
                var embeddedTexts = compilation.SyntaxTrees
                    .Select(t => (filePath: t.FilePath, text: t.GetText()))
                    .Where(t => t.text.CanBeEmbedded && !string.IsNullOrEmpty(t.filePath))
                    .Select(t => EmbeddedText.FromSource(t.filePath, t.text))
                    .ToImmutableArray();
 
                EmitResult result;
                try
                {
                    result = compilation.Emit(
                        executableStream,
                        metadataPEStream: null,
                        pdbStream: pdbStream,
                        xmlDocumentationStream: null,
                        win32Resources: null,
                        manifestResources: manifestResources,
                        options: emitOptions,
                        debugEntryPoint: null,
                        sourceLinkStream: null,
                        embeddedTexts,
                        rebuildData: null,
                        testData: testData,
                        cancellationToken: default);
                }
                finally
                {
                    if (pdbStream != null)
                    {
                        pdb = pdbStream.ToImmutable();
                        pdbStream.Dispose();
                    }
                }
 
                diagnostics.AddRange(result.Diagnostics);
                assembly = executableStream.ToImmutable();
 
                if (result.Success)
                {
                    return new EmitOutput(assembly, pdb);
                }
 
                return null;
            }
        }
 
    }
}