File: Remote\SnapshotSerializationTests.cs
Web Access
Project: src\src\VisualStudio\Core\Test.Next\Roslyn.VisualStudio.Next.UnitTests.csproj (Roslyn.VisualStudio.Next.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.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Remote.Testing;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Remote.UnitTests
{
    [CollectionDefinition(Name)]
    public class AssemblyLoadTestFixtureCollection : ICollectionFixture<AssemblyLoadTestFixture>
    {
        public const string Name = nameof(AssemblyLoadTestFixtureCollection);
        private AssemblyLoadTestFixtureCollection() { }
    }
 
    [Collection(AssemblyLoadTestFixtureCollection.Name)]
    [UseExportProvider]
    public sealed class SnapshotSerializationTests(AssemblyLoadTestFixture testFixture)
    {
        private readonly AssemblyLoadTestFixture _testFixture = testFixture;
 
        private static Workspace CreateWorkspace(Type[] additionalParts = null)
            => new AdhocWorkspace(FeaturesTestCompositions.Features.AddParts(additionalParts).WithTestHostParts(TestHost.OutOfProcess).GetHostServices());
 
        internal static Solution SetFullSolution(Workspace workspace)
        {
            workspace.SetCurrentSolution(solution =>
            {
                var csCode = "class A { }";
                var project1 = solution.AddProject("Project", "Project.dll", LanguageNames.CSharp);
                var document1 = project1.AddDocument("Document1", SourceText.From(csCode));
 
                var vbCode = "Class B\r\nEnd Class";
                var project2 = document1.Project.Solution.AddProject("Project2", "Project2.dll", LanguageNames.VisualBasic);
                var document2 = project2.AddDocument("Document2", SourceText.From(vbCode));
 
                solution = document2.Project.Solution.GetRequiredProject(project1.Id)
                    .AddProjectReference(new ProjectReference(project2.Id, ["test"]))
                    .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
                    .AddAnalyzerReference(new AnalyzerFileReference(Path.Combine(TempRoot.Root, "path1"), new TestAnalyzerAssemblyLoader()))
                    .AddAdditionalDocument("Additional", SourceText.From("hello"), ["test"], @".\Add").Project.Solution;
 
                return solution
                    .WithAnalyzerReferences([new AnalyzerFileReference(Path.Combine(TempRoot.Root, "path2"), new TestAnalyzerAssemblyLoader())])
                    .AddAnalyzerConfigDocuments([DocumentInfo.Create(
                        DocumentId.CreateNewId(project1.Id),
                        ".editorconfig",
                        loader: TextLoader.From(TextAndVersion.Create(SourceText.From("root = true"), VersionStamp.Create())))]);
            }, WorkspaceChangeKind.SolutionChanged);
 
            return workspace.CurrentSolution;
        }
 
        private static async Task<SolutionAsset> GetRequiredAssetAsync(SolutionAssetStorage.Scope scope, Checksum checksum)
        {
            var data = await scope.GetTestAccessor().GetAssetAsync(checksum, CancellationToken.None);
            Contract.ThrowIfNull(data);
            return new(checksum, data);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Empty()
        {
            using var workspace = CreateWorkspace();
            var solution = workspace.CurrentSolution;
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
            var checksum = scope.SolutionChecksum;
            var solutionSyncObject = await GetRequiredAssetAsync(scope, checksum);
 
            await validator.VerifySynchronizationObjectInServiceAsync(solutionSyncObject);
 
            var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(checksum);
            var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
            await validator.VerifyChecksumInServiceAsync(solutionObject.Attributes, WellKnownSynchronizationKind.SolutionAttributes);
 
            Assert.Equal(0, solutionObject.Projects.Length);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Empty_Serialization()
        {
            using var workspace = CreateWorkspace();
            var solution = workspace.CurrentSolution;
 
            var validator = new SerializationValidator(workspace.Services);
            using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
            await validator.VerifySolutionStateSerializationAsync(solution, scope.SolutionChecksum);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Project()
        {
            using var workspace = CreateWorkspace();
            var solution = workspace.CurrentSolution;
            var project = solution.AddProject("Project", "Project.dll", LanguageNames.CSharp);
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
            var checksum = scope.SolutionChecksum;
            var solutionSyncObject = await GetRequiredAssetAsync(scope, checksum);
 
            await validator.VerifySynchronizationObjectInServiceAsync(solutionSyncObject);
 
            var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(checksum);
            var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
 
            await validator.VerifyChecksumInServiceAsync(solutionObject.Attributes, WellKnownSynchronizationKind.SolutionAttributes);
 
            Assert.Equal(1, solutionObject.Projects.Length);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Project_Serialization()
        {
            using var workspace = CreateWorkspace();
            var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", LanguageNames.CSharp);
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var snapshot = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
            await validator.VerifySolutionStateSerializationAsync(project.Solution, snapshot.SolutionChecksum);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId()
        {
            var code = "class A { }";
 
            using var workspace = CreateWorkspace();
            var document = workspace.CurrentSolution.AddProject("Project", "Project.dll", LanguageNames.CSharp).AddDocument("Document", SourceText.From(code));
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(document.Project.Solution, CancellationToken.None);
            var syncObject = await GetRequiredAssetAsync(scope, scope.SolutionChecksum);
            var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(syncObject.Checksum);
            var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
 
            await validator.VerifySynchronizationObjectInServiceAsync(syncObject);
            await validator.VerifyChecksumInServiceAsync(solutionObject.Attributes, WellKnownSynchronizationKind.SolutionAttributes);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Serialization()
        {
            var code = "class A { }";
 
            using var workspace = CreateWorkspace();
            var solution = workspace.CurrentSolution;
            var document = solution.AddProject("Project", "Project.dll", LanguageNames.CSharp).AddDocument("Document", SourceText.From(code));
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(document.Project.Solution, CancellationToken.None);
            await validator.VerifySolutionStateSerializationAsync(document.Project.Solution, scope.SolutionChecksum);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Full()
        {
            using var workspace = CreateWorkspace();
            var solution = SetFullSolution(workspace);
 
            var firstProjectChecksum = await solution.GetProject(solution.ProjectIds[0]).State.GetChecksumAsync(CancellationToken.None);
            var secondProjectChecksum = await solution.GetProject(solution.ProjectIds[1]).State.GetChecksumAsync(CancellationToken.None);
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
            var syncObject = await GetRequiredAssetAsync(scope, scope.SolutionChecksum);
            var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(syncObject.Checksum);
            var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
 
            await validator.VerifySynchronizationObjectInServiceAsync(syncObject);
            await validator.VerifyChecksumInServiceAsync(solutionObject.Attributes, WellKnownSynchronizationKind.SolutionAttributes);
 
            Assert.Equal(2, solutionObject.Projects.Length);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Full_Serialization()
        {
            using var workspace = CreateWorkspace();
            var solution = SetFullSolution(workspace);
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
            await validator.VerifySolutionStateSerializationAsync(solution, scope.SolutionChecksum);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Full_Asset_Serialization()
        {
            using var workspace = CreateWorkspace();
            var solution = SetFullSolution(workspace);
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
            var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope.SolutionChecksum);
            var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
            await validator.VerifyAssetAsync(solutionObject);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Full_Asset_Serialization_Desktop()
        {
            using var workspace = CreateWorkspace();
            var solution = SetFullSolution(workspace);
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
            var solutionCompilationObject = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope.SolutionChecksum);
            var solutionObject = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject.SolutionState);
            await validator.VerifyAssetAsync(solutionObject);
        }
 
        [Fact]
        public async Task CreateSolutionSnapshotId_Duplicate()
        {
            using var workspace = CreateWorkspace();
            var solution = SetFullSolution(workspace);
 
            // this is just data, one can hold the id outside of using statement. but
            // one can't get asset using checksum from the id.
            SolutionCompilationStateChecksums solutionCompilationId1;
            SolutionCompilationStateChecksums solutionCompilationId2;
            SolutionStateChecksums solutionId1;
            SolutionStateChecksums solutionId2;
 
            var validator = new SerializationValidator(workspace.Services);
 
            using (var scope1 = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None).ConfigureAwait(false))
            {
                solutionCompilationId1 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope1.SolutionChecksum);
                solutionId1 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationId1.SolutionState);
            }
 
            using (var scope2 = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None).ConfigureAwait(false))
            {
                solutionCompilationId2 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope2.SolutionChecksum);
                solutionId2 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationId2.SolutionState);
            }
 
            validator.SolutionCompilationStateEqual(solutionCompilationId1, solutionCompilationId2);
 
            // once pinned snapshot scope is released, there is no way to get back to asset.
            // catch Exception because it will throw 2 different exception based on release or debug (ExceptionUtilities.UnexpectedValue)
            Assert.ThrowsAny<Exception>(() => validator.SolutionStateEqual(solutionId1, solutionId2));
        }
 
        [Fact]
        public void MetadataReference_RoundTrip_Test()
        {
            using var workspace = CreateWorkspace();
 
            var reference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
 
            var serializer = workspace.Services.GetService<ISerializerService>();
            var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
 
            var assetFromStorage = CloneAsset(serializer, assetFromFile);
            _ = CloneAsset(serializer, assetFromStorage);
        }
 
        [Fact]
        public async Task Workspace_RoundTrip_Test()
        {
            using var workspace = CreateWorkspace();
            var solution = SetFullSolution(workspace);
 
            var validator = new SerializationValidator(workspace.Services);
 
            var scope1 = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
 
            // recover solution from given snapshot
            var recovered = await validator.GetSolutionAsync(scope1);
            var solutionCompilationObject1 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope1.SolutionChecksum);
            var solutionObject1 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject1.SolutionState);
 
            // create new snapshot from recovered solution
            using var scope2 = await validator.AssetStorage.StoreAssetsAsync(recovered, CancellationToken.None);
 
            // verify asset created by recovered solution is good
            var solutionCompilationObject2 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope2.SolutionChecksum);
            var solutionObject2 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject2.SolutionState);
            await validator.VerifyAssetAsync(solutionObject2);
 
            // verify snapshots created from original solution and recovered solution are same
            validator.SolutionStateEqual(solutionObject1, solutionObject2);
 
            // recover new solution from recovered solution
            var roundtrip = await validator.GetSolutionAsync(scope2);
 
            // create new snapshot from round tripped solution
            using var scope3 = await validator.AssetStorage.StoreAssetsAsync(roundtrip, CancellationToken.None);
            // verify asset created by round trip solution is good
            var solutionCompilationObject3 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope3.SolutionChecksum);
            var solutionObject3 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject3.SolutionState);
            await validator.VerifyAssetAsync(solutionObject3);
 
            // verify snapshots created from original solution and round trip solution are same.
            validator.SolutionStateEqual(solutionObject2, solutionObject3);
        }
 
        [Fact]
        public async Task Workspace_RoundTrip_Test_Desktop()
        {
            using var workspace = CreateWorkspace();
            var solution = SetFullSolution(workspace);
 
            var validator = new SerializationValidator(workspace.Services);
 
            var scope1 = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
 
            // recover solution from given snapshot
            var recovered = await validator.GetSolutionAsync(scope1);
            var solutionCompilationObject1 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope1.SolutionChecksum);
            var solutionObject1 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject1.SolutionState);
 
            // create new snapshot from recovered solution
            using var scope2 = await validator.AssetStorage.StoreAssetsAsync(recovered, CancellationToken.None);
 
            // verify asset created by recovered solution is good
            var solutionCompilationObject2 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope2.SolutionChecksum);
            var solutionObject2 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject2.SolutionState);
            await validator.VerifyAssetAsync(solutionObject2);
 
            // verify snapshots created from original solution and recovered solution are same
            validator.SolutionStateEqual(solutionObject1, solutionObject2);
            scope1.Dispose();
 
            // recover new solution from recovered solution
            var roundtrip = await validator.GetSolutionAsync(scope2);
 
            // create new snapshot from round tripped solution
            using var scope3 = await validator.AssetStorage.StoreAssetsAsync(roundtrip, CancellationToken.None);
            // verify asset created by round trip solution is good
            var solutionCompilationObject3 = await validator.GetValueAsync<SolutionCompilationStateChecksums>(scope3.SolutionChecksum);
            var solutionObject3 = await validator.GetValueAsync<SolutionStateChecksums>(solutionCompilationObject3.SolutionState);
            await validator.VerifyAssetAsync(solutionObject3);
 
            // verify snapshots created from original solution and round trip solution are same.
            validator.SolutionStateEqual(solutionObject2, solutionObject3);
        }
 
        [Fact]
        public void Missing_Metadata_Serialization_Test()
        {
            using var workspace = CreateWorkspace();
            var serializer = workspace.Services.GetService<ISerializerService>();
 
            var reference = new MissingMetadataReference();
 
            // make sure this doesn't throw
            var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
            var assetFromStorage = CloneAsset(serializer, assetFromFile);
            _ = CloneAsset(serializer, assetFromStorage);
        }
 
        [Fact]
        public void Missing_Analyzer_Serialization_Test()
        {
            using var workspace = CreateWorkspace();
            var serializer = workspace.Services.GetService<ISerializerService>();
 
            var reference = new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing_reference"), new MissingAnalyzerLoader());
 
            // make sure this doesn't throw
            var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
            var assetFromStorage = CloneAsset(serializer, assetFromFile);
            _ = CloneAsset(serializer, assetFromStorage);
        }
 
        [Fact]
        public void Missing_Analyzer_Serialization_Desktop_Test()
        {
            using var workspace = CreateWorkspace();
            var serializer = workspace.Services.GetService<ISerializerService>();
 
            var reference = new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing_reference"), new MissingAnalyzerLoader());
 
            // make sure this doesn't throw
            var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
            var assetFromStorage = CloneAsset(serializer, assetFromFile);
            _ = CloneAsset(serializer, assetFromStorage);
        }
 
        [Fact]
        public void RoundTrip_Analyzer_Serialization_Test()
        {
            using var tempRoot = new TempRoot();
            using var workspace = CreateWorkspace();
 
            var serializer = workspace.Services.GetService<ISerializerService>();
 
            // actually shadow copy content
            var location = typeof(object).Assembly.Location;
            var file = tempRoot.CreateFile("shadow", "dll");
            file.CopyContentFrom(location);
 
            var reference = new AnalyzerFileReference(location, new MockShadowCopyAnalyzerAssemblyLoader(ImmutableDictionary<string, string>.Empty.Add(location, file.Path)));
 
            // make sure this doesn't throw
            var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
            var assetFromStorage = CloneAsset(serializer, assetFromFile);
            _ = CloneAsset(serializer, assetFromStorage);
        }
 
        [Fact]
        public void RoundTrip_Analyzer_Serialization_Desktop_Test()
        {
            using var tempRoot = new TempRoot();
 
            using var workspace = CreateWorkspace();
            var serializer = workspace.Services.GetService<ISerializerService>();
 
            // actually shadow copy content
            var location = typeof(object).Assembly.Location;
            var file = tempRoot.CreateFile("shadow", "dll");
            file.CopyContentFrom(location);
 
            var reference = new AnalyzerFileReference(location, new MockShadowCopyAnalyzerAssemblyLoader(ImmutableDictionary<string, string>.Empty.Add(location, file.Path)));
 
            // make sure this doesn't throw
            var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
            var assetFromStorage = CloneAsset(serializer, assetFromFile);
            _ = CloneAsset(serializer, assetFromStorage);
        }
 
        [Fact]
        public void ShadowCopied_Analyzer_Serialization_Desktop_Test()
        {
            using var tempRoot = new TempRoot();
            using var workspace = CreateWorkspace();
            var reference = CreateShadowCopiedAnalyzerReference(tempRoot);
 
            var serializer = workspace.Services.GetService<ISerializerService>();
 
            // make sure this doesn't throw
            var assetFromFile = new SolutionAsset(serializer.CreateChecksum(reference, CancellationToken.None), reference);
 
            // this will verify serialized analyzer reference return same checksum as the original one
            _ = CloneAsset(serializer, assetFromFile);
        }
 
        [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1107294")]
        public async Task SnapshotWithIdenticalAnalyzerFiles()
        {
            using var workspace = CreateWorkspace();
            var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", LanguageNames.CSharp);
 
            using var temp = new TempRoot();
            var dir = temp.CreateDirectory();
 
            // create two analyzer assembly files whose content is identical but path is different:
            var file1 = dir.CreateFile("analyzer1.dll").CopyContentFrom(_testFixture.FaultyAnalyzer);
            var file2 = dir.CreateFile("analyzer2.dll").CopyContentFrom(_testFixture.FaultyAnalyzer);
 
            var analyzer1 = new AnalyzerFileReference(file1.Path, TestAnalyzerAssemblyLoader.LoadNotImplemented);
            var analyzer2 = new AnalyzerFileReference(file2.Path, TestAnalyzerAssemblyLoader.LoadNotImplemented);
 
            project = project.AddAnalyzerReferences([analyzer1, analyzer2]);
 
            var validator = new SerializationValidator(workspace.Services);
            using var snapshot = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
 
            var recovered = await validator.GetSolutionAsync(snapshot);
            AssertEx.Equal([file1.Path, file2.Path], recovered.GetProject(project.Id).AnalyzerReferences.Select(r => r.FullPath));
        }
 
        [Fact]
        public async Task SnapshotWithMissingReferencesTest()
        {
            using var workspace = CreateWorkspace();
            var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", LanguageNames.CSharp);
 
            var metadata = new MissingMetadataReference();
            var analyzer = new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing_reference"), new MissingAnalyzerLoader());
 
            project = project.AddMetadataReference(metadata);
            project = project.AddAnalyzerReference(analyzer);
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var snapshot = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
            // this shouldn't throw
            var recovered = await validator.GetSolutionAsync(snapshot);
        }
 
        [Fact]
        public async Task UnknownLanguageTest()
        {
            using var workspace = CreateWorkspace([typeof(NoCompilationLanguageService)]);
            var project = workspace.CurrentSolution.AddProject("Project", "Project.dll", NoCompilationConstants.LanguageName);
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var snapshot = await validator.AssetStorage.StoreAssetsAsync(project.Solution, CancellationToken.None);
            // this shouldn't throw
            var recovered = await validator.GetSolutionAsync(snapshot);
        }
 
        [Fact]
        public async Task EmptyAssetChecksumTest()
        {
            var document = CreateWorkspace().CurrentSolution.AddProject("empty", "empty", LanguageNames.CSharp).AddDocument("empty", SourceText.From(""));
            var serializer = document.Project.Solution.Services.GetService<ISerializerService>();
 
            var text = await document.GetTextAsync().ConfigureAwait(false);
            var source = new SerializableSourceText(text, text.GetContentHash()).ContentChecksum;
            var metadata = serializer.CreateChecksum(new MissingMetadataReference(), CancellationToken.None);
            var analyzer = serializer.CreateChecksum(new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing"), new MissingAnalyzerLoader()), CancellationToken.None);
 
            Assert.NotEqual(source, metadata);
            Assert.NotEqual(source, analyzer);
            Assert.NotEqual(metadata, analyzer);
        }
 
        [Fact]
        public async Task VBParseOptionsInCompilationOptions()
        {
            var project = CreateWorkspace().CurrentSolution.AddProject("empty", "empty", LanguageNames.VisualBasic);
            project = project.WithCompilationOptions(
                ((VisualBasic.VisualBasicCompilationOptions)project.CompilationOptions).WithParseOptions((VisualBasic.VisualBasicParseOptions)project.ParseOptions));
 
            var checksum = await project.State.GetChecksumAsync(CancellationToken.None);
 
            Assert.True(checksum != Checksum.Null);
        }
 
        [Fact]
        public async Task TestMetadataXmlDocComment()
        {
            using var tempRoot = new TempRoot();
            // get original assembly location
            var mscorlibLocation = typeof(object).Assembly.Location;
 
            // set up dll and xml doc content
            var tempDir = tempRoot.CreateDirectory();
            var tempCorlib = tempDir.CopyFile(mscorlibLocation);
            var tempCorlibXml = tempDir.CreateFile(Path.ChangeExtension(tempCorlib.Path, "xml"));
            tempCorlibXml.WriteAllText(@"<?xml version=""1.0"" encoding=""utf-8""?>
<doc>
  <assembly>
    <name>mscorlib</name>
  </assembly>
  <members>
    <member name=""T:System.Object"">
      <summary>Supports all classes in the .NET Framework class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all classes in the .NET Framework; it is the root of the type hierarchy.To browse the .NET Framework source code for this type, see the Reference Source.</summary>
    </member>
  </members>
</doc>");
 
            using var workspace = CreateWorkspace();
            var solution = workspace.CurrentSolution
                .AddProject("Project", "Project.dll", LanguageNames.CSharp)
                .AddMetadataReference(MetadataReference.CreateFromFile(tempCorlib.Path))
                .Solution;
 
            var validator = new SerializationValidator(workspace.Services);
 
            using var scope = await validator.AssetStorage.StoreAssetsAsync(solution, CancellationToken.None);
            // recover solution from given snapshot
            var recovered = await validator.GetSolutionAsync(scope);
 
            var compilation = await recovered.Projects.First().GetCompilationAsync(CancellationToken.None);
            var objectType = compilation.GetTypeByMetadataName("System.Object");
            var xmlDocComment = objectType.GetDocumentationCommentXml();
 
            Assert.False(string.IsNullOrEmpty(xmlDocComment));
        }
 
        [Fact]
        public void TestEncodingSerialization()
        {
            using var workspace = CreateWorkspace();
 
            var serializer = workspace.Services.GetService<ISerializerService>();
 
            // test with right serializable encoding
            var sourceText = SourceText.From("Hello", Encoding.UTF8);
            var serializableSourceText = new SerializableSourceText(sourceText, sourceText.GetContentHash());
            using (var stream = SerializableBytes.CreateWritableStream())
            {
                using (var objectWriter = new ObjectWriter(stream, leaveOpen: true))
                {
                    serializer.Serialize(serializableSourceText, objectWriter, CancellationToken.None);
                }
 
                stream.Position = 0;
 
                using var objectReader = ObjectReader.TryGetReader(stream);
 
                var newText = (SerializableSourceText)serializer.Deserialize(serializableSourceText.GetWellKnownSynchronizationKind(), objectReader, CancellationToken.None);
                Assert.Equal(sourceText.ToString(), newText.GetText(CancellationToken.None).ToString());
            }
 
            // test with wrong encoding that doesn't support serialization
            sourceText = SourceText.From("Hello", new NotSerializableEncoding());
            serializableSourceText = new SerializableSourceText(sourceText, sourceText.GetContentHash());
            using (var stream = SerializableBytes.CreateWritableStream())
            {
                using (var objectWriter = new ObjectWriter(stream, leaveOpen: true))
                {
                    serializer.Serialize(serializableSourceText, objectWriter, CancellationToken.None);
                }
 
                stream.Position = 0;
 
                using var objectReader = ObjectReader.TryGetReader(stream);
 
                var newText = (SerializableSourceText)serializer.Deserialize(serializableSourceText.GetWellKnownSynchronizationKind(), objectReader, CancellationToken.None);
                Assert.Equal(sourceText.ToString(), newText.GetText(CancellationToken.None).ToString());
            }
        }
 
        [Fact]
        public void TestCompilationOptions_NullableAndImport()
        {
            var csharpOptions = CSharp.CSharpCompilation.Create("dummy").Options.WithNullableContextOptions(NullableContextOptions.Warnings).WithMetadataImportOptions(MetadataImportOptions.All);
            var vbOptions = VisualBasic.VisualBasicCompilation.Create("dummy").Options.WithMetadataImportOptions(MetadataImportOptions.Internal);
 
            using var workspace = CreateWorkspace();
            var serializer = workspace.Services.GetService<ISerializerService>();
 
            VerifyOptions(csharpOptions);
            VerifyOptions(vbOptions);
 
            void VerifyOptions(CompilationOptions originalOptions)
            {
                using var stream = SerializableBytes.CreateWritableStream();
                using (var objectWriter = new ObjectWriter(stream, leaveOpen: true))
                {
                    serializer.Serialize(originalOptions, objectWriter, CancellationToken.None);
                }
 
                stream.Position = 0;
                using var objectReader = ObjectReader.TryGetReader(stream);
                var recoveredOptions = (CompilationOptions)serializer.Deserialize(originalOptions.GetWellKnownSynchronizationKind(), objectReader, CancellationToken.None);
 
                var original = serializer.CreateChecksum(originalOptions, CancellationToken.None);
                var recovered = serializer.CreateChecksum(recoveredOptions, CancellationToken.None);
 
                Assert.Equal(original, recovered);
            }
        }
 
        private static SolutionAsset CloneAsset(ISerializerService serializer, SolutionAsset asset)
        {
            using var stream = SerializableBytes.CreateWritableStream();
            using (var writer = new ObjectWriter(stream, leaveOpen: true))
            {
                serializer.Serialize(asset.Value, writer, CancellationToken.None);
            }
 
            stream.Position = 0;
            using var reader = ObjectReader.TryGetReader(stream);
            var recovered = serializer.Deserialize(asset.Kind, reader, CancellationToken.None);
            var checksum = recovered is SerializableSourceText text ? text.ContentChecksum : serializer.CreateChecksum(recovered, CancellationToken.None);
 
            var assetFromStorage = new SolutionAsset(checksum, recovered);
 
            Assert.Equal(asset.Checksum, assetFromStorage.Checksum);
            return assetFromStorage;
        }
 
        private static AnalyzerFileReference CreateShadowCopiedAnalyzerReference(TempRoot tempRoot)
        {
            // use 2 different files as shadow copied content
            var original = typeof(AdhocWorkspace).Assembly.Location;
 
            var shadow = tempRoot.CreateFile("shadow", "dll");
            shadow.CopyContentFrom(typeof(object).Assembly.Location);
 
            return new AnalyzerFileReference(original, new MockShadowCopyAnalyzerAssemblyLoader(ImmutableDictionary<string, string>.Empty.Add(original, shadow.Path)));
        }
 
        private class MissingAnalyzerLoader() : AnalyzerAssemblyLoader([])
        {
            protected override string PreparePathToLoad(string fullPath)
                => throw new FileNotFoundException(fullPath);
 
            protected override string PrepareSatelliteAssemblyToLoad(string fullPath, string cultureName)
                => throw new FileNotFoundException(fullPath);
        }
 
        private class MissingMetadataReference : PortableExecutableReference
        {
            public MissingMetadataReference()
                : base(MetadataReferenceProperties.Assembly, "missing_reference", XmlDocumentationProvider.Default)
            {
            }
 
            protected override DocumentationProvider CreateDocumentationProvider()
                => null;
 
            protected override Metadata GetMetadataImpl()
                => throw new FileNotFoundException("can't find");
 
            protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties)
                => this;
        }
 
        private class MockShadowCopyAnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
        {
            private readonly ImmutableDictionary<string, string> _map;
 
            public MockShadowCopyAnalyzerAssemblyLoader(ImmutableDictionary<string, string> map)
                => _map = map;
 
            public void AddDependencyLocation(string fullPath)
            {
            }
 
            public Assembly LoadFromPath(string fullPath)
                => Assembly.LoadFrom(_map[fullPath]);
        }
 
        private class NotSerializableEncoding : Encoding
        {
            private readonly Encoding _real = Encoding.UTF8;
 
            public override string WebName => _real.WebName;
            public override int GetByteCount(char[] chars, int index, int count) => _real.GetByteCount(chars, index, count);
            public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) => GetBytes(chars, charIndex, charCount, bytes, byteIndex);
            public override int GetCharCount(byte[] bytes, int index, int count) => GetCharCount(bytes, index, count);
            public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) => GetChars(bytes, byteIndex, byteCount, chars, charIndex);
            public override int GetMaxByteCount(int charCount) => GetMaxByteCount(charCount);
            public override int GetMaxCharCount(int byteCount) => GetMaxCharCount(byteCount);
        }
    }
}