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 sealed 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 sealed class MissingAnalyzerLoader() : IAnalyzerAssemblyLoader
    {
        public void AddDependencyLocation(string fullPath)
        {
        }
 
        public Assembly LoadFromPath(string fullPath)
            => throw new FileNotFoundException(fullPath);
    }
 
    private sealed 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 sealed 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 sealed 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);
    }
}