File: DeterministicKeyBuilderTests.cs
Web Access
Project: src\src\Compilers\Core\RebuildTest\Microsoft.CodeAnalysis.Rebuild.UnitTests.csproj (Microsoft.CodeAnalysis.Rebuild.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.Runtime.InteropServices;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.VisualBasic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Roslyn.Test.Utilities;
using Roslyn.Test.Utilities.TestGenerators;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Rebuild.UnitTests
{
    public abstract partial class DeterministicKeyBuilderTests<TCompilation, TCompilationOptions, TParseOptions>
        where TCompilation : Compilation
        where TCompilationOptions : CompilationOptions
        where TParseOptions : ParseOptions
    {
        private static readonly char[] s_trimChars = { ' ', '\n', '\r' };
 
        public static EmitOptions EmitOptions { get; } = new();
        public static SourceHashAlgorithm HashAlgorithm { get; } = SourceHashAlgorithm.Sha256;
        public static SourceHashAlgorithm[] HashAlgorithms { get; } = new[]
        {
            SourceHashAlgorithm.Sha1,
            SourceHashAlgorithm.Sha256
        };
 
        protected static void AssertJson(
            string expected,
            string actual) => AssertJson(expected, actual, "references", "extensions");
 
        protected static void AssertJson(
            string expected,
            string actual,
            params string[] ignoreSections)
        {
            var json = JObject.Parse(actual);
            if (ignoreSections.Length > 0)
            {
                json
                    .Descendants()
                    .OfType<JProperty>()
                    .Where(x => ignoreSections.Contains(x.Name))
                    .ToList()
                    .ForEach(x => x.Remove());
            }
 
            actual = json.ToString(Formatting.Indented);
            expected = JObject.Parse(expected).ToString(Formatting.Indented);
            AssertJsonCore(expected, actual);
        }
 
        protected static void AssertJsonSection(
            string expected,
            string actual,
            string sectionName,
            params string[] ignoreProperties)
        {
            var property = GetJsonProperty(actual, sectionName, ignoreProperties);
            AssertJsonCore(expected, property.ToString(Formatting.Indented));
        }
 
        protected static void AssertJsonCore(string? expected, string? actual)
        {
            expected = expected?.Trim(s_trimChars);
            actual = actual?.Trim(s_trimChars);
            Assert.Equal(expected, actual);
        }
 
        private protected static void AssertSyntaxTreePathMap(string? expected, CommonCompiler compiler)
        {
            Assert.Empty(compiler.Arguments.Errors);
 
            var writer = new StringWriter();
            var compilation = compiler.CreateCompilation(
                writer,
                touchedFilesLogger: null,
                errorLoggerOpt: null,
                analyzerConfigOptions: default,
                globalConfigOptions: default);
            AssertEx.NotNull(compilation);
            Assert.Empty(writer.GetStringBuilder().ToString());
            var obj = GetSyntaxTreeValues(compilation, compiler.Arguments.PathMap);
            AssertJsonCore(expected, obj.ToString(Formatting.Indented));
        }
 
        protected static JProperty GetJsonProperty(
            string json,
            string sectionName,
            params string[] ignoreProperties)
        {
            var lastName = sectionName.Split('.').Last();
            var property = JObject.Parse(json)
                .Descendants()
                .OfType<JProperty>()
                .Where(x => x.Name == lastName && getFullName(x) == sectionName)
                .Single();
 
            if (ignoreProperties.Length > 0)
            {
                if (property.Value is JObject value)
                {
                    removeProperties(value);
                }
                else if (property.Value is JArray array)
                {
                    foreach (var element in array.Values<JObject>())
                    {
                        removeProperties(element!);
                    }
                }
                else
                {
                    throw new InvalidOperationException();
                }
 
                void removeProperties(JObject value)
                {
                    foreach (var ignoreProperty in ignoreProperties)
                    {
                        value.Properties().Where(x => x.Name == ignoreProperty).Single().Remove();
                    }
                }
            }
 
            return property;
 
            static string getFullName(JProperty property)
            {
                string name = property.Name;
                while (property.Parent is JObject obj)
                {
                    if (obj.Parent is JProperty parent)
                    {
                        name = $"{parent.Name}.{name}";
                        property = parent;
                    }
                    else if (obj.Parent is JArray { Parent: JProperty arrayParent } array)
                    {
                        name = $"[].{name}";
                        property = arrayParent;
                    }
                    else
                    {
                        break;
                    }
                }
 
                return name;
            }
        }
 
        protected JObject GetCompilationOptionsValue(CompilationOptions options)
        {
            var compilation = CreateCompilation(syntaxTrees: new SyntaxTree[] { }, options: (TCompilationOptions)options);
            var property = GetJsonProperty(compilation.GetDeterministicKey(), "compilation.options");
            return (JObject)property.Value;
        }
 
        protected JObject GetCompilationValue(Compilation compilation)
        {
            var property = GetJsonProperty(compilation.GetDeterministicKey(), "compilation");
            return (JObject)property.Value;
        }
 
        protected JObject GetParseOptionsValue(ParseOptions parseOptions)
        {
            var syntaxTree = ParseSyntaxTree("", fileName: "test", SourceHashAlgorithm.Sha256, (TParseOptions)parseOptions);
            var compilation = CreateCompilation(syntaxTrees: new SyntaxTree[] { syntaxTree });
            var property = GetJsonProperty(compilation.GetDeterministicKey(), "compilation.syntaxTrees");
            var trees = (JArray)property.Value;
            var obj = (JObject)trees[0];
            return (JObject)(obj.Property("parseOptions")?.Value!);
        }
 
        protected JArray GetReferenceValues(Compilation compilation)
        {
            var property = GetJsonProperty(compilation.GetDeterministicKey(), "compilation.references");
            return (JArray)property.Value;
        }
 
        protected JObject GetReferenceValue(MetadataReference reference)
        {
            var expectedMvid = DeterministicKeyBuilder.GetGuidValue(reference.GetModuleVersionId());
            var compilation = CreateCompilation(syntaxTrees: new SyntaxTree[] { }, references: new[] { reference });
            var array = GetReferenceValues(compilation);
 
            foreach (var item in array!.Values<JObject>())
            {
                if (item?.Value<string>("mvid") == expectedMvid)
                {
                    return item;
                }
            }
 
            Assert.True(false, $"Could not find reference with MVID {expectedMvid}");
            throw null!;
        }
 
        protected static JArray GetSyntaxTreeValues(Compilation compilation, ImmutableArray<KeyValuePair<string, string>> pathMap = default)
        {
            var property = GetJsonProperty(compilation.GetDeterministicKey(pathMap: pathMap), "compilation.syntaxTrees");
            return (JArray)property.Value; ;
        }
 
        protected static JArray GetAdditionalTextValues(Compilation compilation, ImmutableArray<AdditionalText> additionalTexts, ImmutableArray<KeyValuePair<string, string>> pathMap = default)
        {
            var property = GetJsonProperty(compilation.GetDeterministicKey(additionalTexts: additionalTexts, pathMap: pathMap), "additionalTexts");
            return (JArray)property.Value;
        }
 
        protected static JArray GetAnalyzerValues(Compilation compilation, params DiagnosticAnalyzer[] analyzers)
        {
            var property = GetJsonProperty(compilation.GetDeterministicKey(analyzers: analyzers.ToImmutableArray()), "analyzers");
            return (JArray)property.Value;
        }
 
        protected static JArray GetGeneratorValues(Compilation compilation, params ISourceGenerator[] generators)
        {
            var property = GetJsonProperty(compilation.GetDeterministicKey(generators: generators.ToImmutableArray()), "generators");
            return (JArray)property.Value;
        }
 
        private protected JObject GetEmitOptionsValue(
            EmitOptions emitOptions,
            ImmutableArray<KeyValuePair<string, string>> pathMap = default,
            DeterministicKeyOptions options = default)
        {
            var compilation = CreateCompilation(new SyntaxTree[] { });
            var key = compilation.GetDeterministicKey(
                emitOptions: emitOptions,
                pathMap: pathMap,
                options: options);
            var property = GetJsonProperty(key, "emitOptions");
            return (JObject)property.Value;
        }
 
        protected static string GetChecksum(SourceText text)
        {
            var checksum = text.GetChecksum();
            var builder = PooledStringBuilder.GetInstance();
            DeterministicKeyBuilder.EncodeByteArrayValue(checksum.AsSpan(), builder);
            return builder.ToStringAndFree();
        }
 
        protected abstract SyntaxTree ParseSyntaxTree(string content, string fileName, SourceHashAlgorithm hashAlgorithm, TParseOptions? parseOptions = null);
 
        protected abstract TCompilation CreateCompilation(
            SyntaxTree[] syntaxTrees,
            MetadataReference[]? references = null,
            TCompilationOptions? options = null);
 
        protected abstract TCompilationOptions GetCompilationOptions();
 
        protected abstract TParseOptions GetParseOptions();
 
        private protected abstract DeterministicKeyBuilder GetDeterministicKeyBuilder();
 
        [Theory]
        [InlineData(@"hello world")]
        [InlineData(@"just need some text here")]
        [InlineData(@"yet another case")]
        public void SyntaxTreeContent(string content)
        {
            foreach (var hashAlgorithm in HashAlgorithms)
            {
                var syntaxTree = ParseSyntaxTree(content, fileName: "file.cs", hashAlgorithm);
                var contentChecksum = GetChecksum(syntaxTree.GetText());
                var compilation = CreateCompilation(new[] { syntaxTree });
                var key = compilation.GetDeterministicKey();
                var expected = @$"
""syntaxTrees"": [
  {{
    ""fileName"": ""file.cs"",
    ""text"": {{
      ""checksum"": ""{contentChecksum}"",
      ""checksumAlgorithm"": ""{hashAlgorithm}"",
      ""encodingName"": ""Unicode (UTF-8)""
    }}
  }}
]";
                AssertJsonSection(expected, key, "compilation.syntaxTrees", "parseOptions");
            }
        }
 
        [Theory]
        [CombinatorialData]
        public void CompilationOptionsCombination(
            OutputKind outputKind,
            bool delaySign,
            bool publicSign,
            bool deterministic)
        {
            var options = GetCompilationOptions()
                .WithOutputKind(outputKind)
                .WithDelaySign(delaySign)
                .WithPublicSign(publicSign)
                .WithDeterministic(deterministic);
 
            var obj = GetCompilationOptionsValue(options);
            Assert.Equal(outputKind.ToString(), obj.Value<string>("outputKind"));
            Assert.Equal(publicSign, obj.Value<bool>("publicSign"));
            Assert.Equal(delaySign, obj.Value<bool>("delaySign"));
            Assert.Equal(deterministic, obj.Value<bool>("deterministic"));
        }
 
        /// <summary>
        /// Makes sure that local time is not encoded for deterministic builds. Otherwise deterministic
        /// builds would not have deterministic keys
        /// </summary>
        [Fact]
        public void CompilationOptionsDeterministic()
        {
            var obj = getValue(deterministic: true);
            Assert.Null(obj.Value<string>("localtime"));
 
            obj = getValue(deterministic: false);
            Assert.NotNull(obj.Value<string>("localtime"));
 
            JObject getValue(bool deterministic)
            {
                var options = GetCompilationOptions()
                    .WithDeterministic(deterministic);
 
                return GetCompilationOptionsValue(options);
            }
        }
 
        /// <summary>
        /// Disabling determinism should mean all calls to GetDeterministicKey return different values.
        /// </summary>
        [Fact]
        public void CompilationOptionsDeterministicOff()
        {
            var options = GetCompilationOptions();
            var compilation = CreateCompilation(syntaxTrees: new SyntaxTree[] { }, options: options);
            var key = compilation.GetDeterministicKey();
 
            Assert.Equal(key, compilation.GetDeterministicKey());
 
            options = (TCompilationOptions)options.WithDeterministic(false);
            compilation = (TCompilation)compilation.WithOptions(options);
            key = compilation.GetDeterministicKey();
            Assert.NotEqual(key, compilation.GetDeterministicKey());
        }
 
        /// <summary>
        /// Verify that options which don't impact determinism are excluded from the key
        /// </summary>
        [Theory]
        [CombinatorialData]
        public void CompilationOptionsExcluded(bool concurrentBuild, MetadataImportOptions metaImportOptions)
        {
            var options = GetCompilationOptions();
            var other = options
                .WithConcurrentBuild(concurrentBuild)
                .WithMetadataImportOptions(metaImportOptions);
 
            var expected = GetCompilationOptionsValue(options);
            var actual = GetCompilationOptionsValue(other);
            Assert.Equal(expected.ToString(), actual.ToString());
        }
 
        [Fact]
        public void CompilationOptionsSpecificDiagnosticOptions()
        {
            assert(@"[]");
            assert(@"
[
  {
    ""CA109"": ""Error""
  }
]"[
  {
    ""CA109"": ""Error""
  }
]", ("CA109", ReportDiagnostic.Error));
 
            assert(@"
[
  {
    ""CA109"": ""Error""
  },
  {
    ""CA200"": ""Warn""
  }
]"[
  {
    ""CA109"": ""Error""
  },
  {
    ""CA200"": ""Warn""
  }
]", ("CA109", ReportDiagnostic.Error), ("CA200", ReportDiagnostic.Warn));
 
            void assert(string expected, params (string Diagnostic, ReportDiagnostic ReportDiagnostic)[] values)
            {
                var map = values.ToImmutableDictionary(
                    x => x.Diagnostic,
                    x => x.ReportDiagnostic);
 
                var options = GetCompilationOptions()
                    .WithSpecificDiagnosticOptions(map);
                var value = GetCompilationOptionsValue(options);
                var actual = value["specificDiagnosticOptions"]?.ToString(Formatting.Indented);
                AssertJsonCore(expected, actual);
            }
        }
 
        [Theory]
        [CombinatorialData]
        public void ParseOptionsCombination(
            SourceCodeKind sourceCodeKind,
            DocumentationMode documentationMode)
        {
            var parseOptions = GetParseOptions()
                .WithKind(sourceCodeKind)
                .WithDocumentationMode(documentationMode);
 
#pragma warning disable 618
            if (sourceCodeKind == SourceCodeKind.Interactive)
            {
                sourceCodeKind = SourceCodeKind.Script;
            }
#pragma warning restore 618
 
            var obj = GetParseOptionsValue(parseOptions);
            Assert.Equal(sourceCodeKind.ToString(), obj.Value<string>("kind"));
            Assert.Equal(documentationMode.ToString(), obj.Value<string>("documentationMode"));
            Assert.Empty(obj.Value<JObject>("features")!.Properties());
        }
 
        [Fact]
        public void ParseOptionsFeatures()
        {
            var parseOptions = GetParseOptions();
 
            assert("{}");
            assert(@"
{
  ""key"": ""value""
}"{
  ""key"": ""value""
}", ("key", "value"));
 
            assert(@"
{
  ""k1"": ""v1"",
  ""k2"": ""v2""
}"{
  ""k1"": ""v1"",
  ""k2"": ""v2""
}", ("k1", "v1"), ("k2", "v2"));
 
            // Same case but reverse the order the keys are added. That should not change the key
            assert(@"
{
  ""k1"": ""v1"",
  ""k2"": ""v2""
}"{
  ""k1"": ""v1"",
  ""k2"": ""v2""
}", ("k2", "v2"), ("k1", "v1"));
 
            // Make sure that the keys are escaped properly
            assert(@"
{
  ""\\\""strange"": ""value""
}"{
  ""\\\""strange"": ""value""
}", (@"\""strange", "value"));
 
            void assert(string? expected, params (string Key, string Value)[] features)
            {
                var parseOptions = GetParseOptions()
                    .WithFeatures(features.Select(x => new KeyValuePair<string, string>(x.Key, x.Value)));
 
                var obj = GetParseOptionsValue(parseOptions);
                var value = obj.Value<JObject>("features");
                AssertJsonCore(expected, value?.ToString(Formatting.Indented));
            }
        }
 
        [Fact]
        public void EmitOptionsDefault()
        {
            var obj = GetEmitOptionsValue(EmitOptions);
            AssertJson(@"
{
  ""emitMetadataOnly"": false,
  ""tolerateErrors"": false,
  ""includePrivateMembers"": true,
  ""instrumentationKinds"": [
  ],
  ""subsystemVersion"": {
    ""major"": 0,
    ""minor"": 0
  },
  ""fileAlignment"": 0,
  ""highEntropyVirtualAddressSpace"": false,
  ""baseAddress"": ""0"",
  ""debugInformationFormat"": ""Pdb"",
  ""outputNameOverride"": null,
  ""pdbFilePath"": null,
  ""pdbChecksumAlgorithm"": ""SHA256"",
  ""runtimeMetadataVersion"": null,
  ""defaultSourceFileEncoding"": null,
  ""fallbackSourceFileEncoding"": null
}
"{
  ""emitMetadataOnly"": false,
  ""tolerateErrors"": false,
  ""includePrivateMembers"": true,
  ""instrumentationKinds"": [
  ],
  ""subsystemVersion"": {
    ""major"": 0,
    ""minor"": 0
  },
  ""fileAlignment"": 0,
  ""highEntropyVirtualAddressSpace"": false,
  ""baseAddress"": ""0"",
  ""debugInformationFormat"": ""Pdb"",
  ""outputNameOverride"": null,
  ""pdbFilePath"": null,
  ""pdbChecksumAlgorithm"": ""SHA256"",
  ""runtimeMetadataVersion"": null,
  ""defaultSourceFileEncoding"": null,
  ""fallbackSourceFileEncoding"": null
}
", obj.ToString(Formatting.Indented));
        }
 
        [Theory]
        [CombinatorialData]
        public void EmitOptionsCombo(
            DebugInformationFormat debugInformationFormat,
            InstrumentationKind kind)
        {
            var emitOptions = EmitOptions
                .Default
                .WithDebugInformationFormat(debugInformationFormat)
                .WithInstrumentationKinds(ImmutableArray.Create(kind));
 
            var obj = GetEmitOptionsValue(emitOptions);
            AssertJson(@$"
{{
  ""emitMetadataOnly"": false,
  ""tolerateErrors"": false,
  ""includePrivateMembers"": true,
  ""instrumentationKinds"": [
    ""{kind}"",
  ],
  ""subsystemVersion"": {{
    ""major"": 0,
    ""minor"": 0
  }},
  ""fileAlignment"": 0,
  ""highEntropyVirtualAddressSpace"": false,
  ""baseAddress"": ""0"",
  ""debugInformationFormat"": ""{debugInformationFormat}"",
  ""outputNameOverride"": null,
  ""pdbFilePath"": null,
  ""pdbChecksumAlgorithm"": ""SHA256"",
  ""runtimeMetadataVersion"": null,
  ""defaultSourceFileEncoding"": null,
  ""fallbackSourceFileEncoding"": null
}}
", obj.ToString(Formatting.Indented));
        }
 
        [Theory]
        [InlineData(1, 2)]
        [InlineData(3, 4)]
        public void EmitOptionsSubsystemVersion(int major, int minor)
        {
            var emitOptions = EmitOptions.WithSubsystemVersion(SubsystemVersion.Create(major, minor));
            var obj = GetEmitOptionsValue(emitOptions);
            var expected = @$"
""subsystemVersion"": {{
  ""major"": {major},
  ""minor"": {minor}
}}";
            AssertJsonSection(expected, obj.ToString(Formatting.Indented), "subsystemVersion");
        }
 
        [Fact]
        public void EmitOptionsPdbFilePathRespectsOptions()
        {
            var path = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                ? @"c:\temp\util.pdb"
                : "/temp/util.pdb";
            var emitOptions = EmitOptions.WithPdbFilePath(path);
            var obj = GetEmitOptionsValue(emitOptions, options: DeterministicKeyOptions.IgnorePaths);
            Assert.Equal(@"util.pdb", obj.Value<string>("pdbFilePath"));
        }
 
        [Theory]
        [InlineData(@"c:\src\util.pdb", null, null)]
        [InlineData(@"d:\src\util.pdb", @"d:\", @"c:\")]
        [InlineData(@"d:\long\src\util.pdb", @"d:\long\", @"c:\")]
        public void EmitOptionsPdbFilePathRespectsPathMap(string filePath, string? pathMapFrom, string? pathMapTo)
        {
            var pathMap = (pathMapFrom, pathMapTo) switch
            {
                (null, null) => ImmutableArray<KeyValuePair<string, string>>.Empty,
                (string, string) => ImmutableArray.Create(KeyValuePairUtil.Create(pathMapFrom, pathMapTo)),
                _ => throw new InvalidOperationException(),
            };
            var emitOptions = EmitOptions.WithPdbFilePath(filePath);
            var obj = GetEmitOptionsValue(emitOptions, pathMap);
            Assert.Equal(@"c:\src\util.pdb", obj.Value<string>("pdbFilePath"));
        }
 
        [Fact]
        public void MetadataReferenceMscorlib()
        {
            var mscorlib = NetCoreApp.mscorlib;
            var obj = GetReferenceValue(mscorlib);
 
            var mvid = DeterministicKeyBuilder.GetGuidValue(mscorlib.GetModuleVersionId());
            var expected = $@"
{{
  ""name"": ""mscorlib"",
  ""version"": {{
    ""major"": 4,
    ""minor"": 0,
    ""build"": 0,
    ""revision"": 0
  }},
  ""publicKey"": ""0000000040000000"",
  ""mvid"": ""{mvid}"",
  ""secondaryModules"": [],
  ""properties"": {{
    ""kind"": ""Assembly"",
    ""embedInteropTypes"": false,
    ""aliases"": []
  }}
}}
";
 
            AssertJsonCore(expected, obj.ToString(Formatting.Indented));
        }
 
        [Fact]
        public void MetadataReferenceMscorlibWithAlias()
        {
            var mscorlib = NetCoreApp.mscorlib.WithAliases(new[] { "alias1", "alias2" });
            var obj = GetReferenceValue(mscorlib);
 
            var mvid = DeterministicKeyBuilder.GetGuidValue(mscorlib.GetModuleVersionId());
            var expected = $@"
{{
  ""name"": ""mscorlib"",
  ""version"": {{
    ""major"": 4,
    ""minor"": 0,
    ""build"": 0,
    ""revision"": 0
  }},
  ""publicKey"": ""0000000040000000"",
  ""mvid"": ""{mvid}"",
  ""secondaryModules"": [],
  ""properties"": {{
    ""kind"": ""Assembly"",
    ""embedInteropTypes"": false,
    ""aliases"": [
      ""alias1"",
      ""alias2""
    ]
  }}
}}
";
 
            AssertJsonCore(expected, obj.ToString(Formatting.Indented));
        }
 
        [Theory]
        [CombinatorialData]
        public void MetadataReferenceMscorlibEmbedInteropTypes(bool embedInteropTypes)
        {
            var mscorlib = NetCoreApp.mscorlib.WithEmbedInteropTypes(embedInteropTypes);
            var obj = GetReferenceValue(mscorlib);
 
            var mvid = DeterministicKeyBuilder.GetGuidValue(mscorlib.GetModuleVersionId());
            var expected = $@"
{{
  ""name"": ""mscorlib"",
  ""version"": {{
    ""major"": 4,
    ""minor"": 0,
    ""build"": 0,
    ""revision"": 0
  }},
  ""publicKey"": ""0000000040000000"",
  ""mvid"": ""{mvid}"",
  ""secondaryModules"": [],
  ""properties"": {{
    ""kind"": ""Assembly"",
    ""embedInteropTypes"": {embedInteropTypes.ToString().ToLowerInvariant()},
    ""aliases"": []
  }}
}}
";
 
            AssertJsonCore(expected, obj.ToString(Formatting.Indented));
        }
 
        [Fact]
        public void MetadataReferenceMultiModule()
        {
            var reference = TestReferences.SymbolsTests.MultiModule.Assembly;
            var obj = GetReferenceValue(reference);
 
            var expected = @"
{
  ""name"": ""MultiModule"",
  ""version"": {
    ""major"": 0,
    ""minor"": 0,
    ""build"": 0,
    ""revision"": 0
  },
  ""publicKey"": """",
  ""mvid"": ""0f07ef6c-4b63-421d-870e-1358db815764"",
  ""secondaryModules"": [
    {
      ""name"": ""mod2.netmodule"",
      ""mvid"": ""82f316ce-66f1-45b6-a2c7-b4476bda03fd""
    },
    {
      ""name"": ""mod3.netmodule"",
      ""mvid"": ""ff9f3a02-95e7-44b2-a278-e9149fa82ee4""
    }
  ],
  ""properties"": {
    ""kind"": ""Assembly"",
    ""embedInteropTypes"": false,
    ""aliases"": []
  }
}"{
  ""name"": ""MultiModule"",
  ""version"": {
    ""major"": 0,
    ""minor"": 0,
    ""build"": 0,
    ""revision"": 0
  },
  ""publicKey"": """",
  ""mvid"": ""0f07ef6c-4b63-421d-870e-1358db815764"",
  ""secondaryModules"": [
    {
      ""name"": ""mod2.netmodule"",
      ""mvid"": ""82f316ce-66f1-45b6-a2c7-b4476bda03fd""
    },
    {
      ""name"": ""mod3.netmodule"",
      ""mvid"": ""ff9f3a02-95e7-44b2-a278-e9149fa82ee4""
    }
  ],
  ""properties"": {
    ""kind"": ""Assembly"",
    ""embedInteropTypes"": false,
    ""aliases"": []
  }
}";
 
            AssertJsonCore(expected, obj.ToString(Formatting.Indented));
        }
 
        [Theory]
        [InlineData(@"c:\src\data.txt", null, null)]
        [InlineData(@"d:\src\data.txt", @"d:\", @"c:\")]
        [InlineData(@"d:\long\src\data.txt", @"d:\long\", @"c:\")]
        public void PathMapAdditionalFiles(string filePath, string? pathMapFrom, string? pathMapTo)
        {
            var pathMap = (pathMapFrom, pathMapTo) switch
            {
                (null, null) => ImmutableArray<KeyValuePair<string, string>>.Empty,
                (string, string) => ImmutableArray.Create(KeyValuePairUtil.Create(pathMapFrom, pathMapTo)),
                _ => throw new InvalidOperationException(),
            };
 
            var additionalText = new InMemoryAdditionalText(filePath, "hello world");
            var array = GetAdditionalTextValues(
                CreateCompilation(new SyntaxTree[] { }),
                ImmutableArray.Create<AdditionalText>(additionalText),
                pathMap);
 
            var expected = @"
[
  {
    ""fileName"": ""c:\\src\\data.txt"",
    ""text"": {
      ""checksum"": ""8f8ceeeb5e1b799fe3c7dd9f059bf8852c57cb"",
      ""checksumAlgorithm"": ""Sha1"",
      ""encodingName"": ""Unicode (UTF-8)""
    }
  }
]
"[
  {
    ""fileName"": ""c:\\src\\data.txt"",
    ""text"": {
      ""checksum"": ""8f8ceeeb5e1b799fe3c7dd9f059bf8852c57cb"",
      ""checksumAlgorithm"": ""Sha1"",
      ""encodingName"": ""Unicode (UTF-8)""
    }
  }
]
";
 
            AssertJsonCore(expected, array.ToString(Formatting.Indented));
        }
 
        [Fact]
        public void AdditionalTextError()
        {
            var additionalText = new TestAdditionalText(path: @"test.txt", text: null);
            var array = GetAdditionalTextValues(
                CreateCompilation(new SyntaxTree[] { }),
                ImmutableArray.Create<AdditionalText>(additionalText));
 
            var expected = @"
[
  {
    ""fileName"": ""test.txt"",
    ""text"": null
  }
]
"[
  {
    ""fileName"": ""test.txt"",
    ""text"": null
  }
]
";
 
            AssertJsonCore(expected, array.ToString(Formatting.Indented));
        }
 
        [Fact]
        public void Analyzers()
        {
            var array = GetAnalyzerValues(
                CreateCompilation(Array.Empty<SyntaxTree>()),
                new Analyzer(),
                new Analyzer2());
 
            var assembly = typeof(Analyzer).Assembly;
            var expected = @$"
[
  {{
    ""fullName"": ""{typeof(Analyzer).FullName}"",
    ""assemblyName"": ""{assembly.FullName}"",
    ""mvid"": ""{DeterministicKeyBuilder.GetGuidValue(assembly.ManifestModule.ModuleVersionId)}""
  }},
  {{
    ""fullName"": ""{typeof(Analyzer2).FullName}"",
    ""assemblyName"": ""{assembly.FullName}"",
    ""mvid"": ""{DeterministicKeyBuilder.GetGuidValue(assembly.ManifestModule.ModuleVersionId)}""
  }}
]
";
            AssertJsonCore(expected, array.ToString(Formatting.Indented));
        }
 
        [Fact]
        public void Generators()
        {
            var array = GetGeneratorValues(
                CreateCompilation(Array.Empty<SyntaxTree>()),
                new Generator(),
                new Generator2());
 
            var assembly = typeof(Generator).Assembly;
            var expected = @$"
[
  {{
    ""fullName"": ""{typeof(Generator).FullName}"",
    ""assemblyName"": ""{assembly.FullName}"",
    ""mvid"": ""{DeterministicKeyBuilder.GetGuidValue(assembly.ManifestModule.ModuleVersionId)}""
  }},
  {{
    ""fullName"": ""{typeof(Generator2).FullName}"",
    ""assemblyName"": ""{assembly.FullName}"",
    ""mvid"": ""{DeterministicKeyBuilder.GetGuidValue(assembly.ManifestModule.ModuleVersionId)}""
  }}
]
";
            AssertJsonCore(expected, array.ToString(Formatting.Indented));
        }
    }
}