/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs
1 363 строки
72 KB
Jason Malinowski
Make SolutionChecksumUpdater disposable and dispose the queues
04 авг 2026, 23:51
04 авг 2026, 23:51
618526f
Код
Авторство
О чём код?
// 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.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editor.Test; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Remote.Testing; using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; namespace Roslyn.VisualStudio.Next.UnitTests.Remote; [UseExportProvider] [Trait(Traits.Feature, Traits.Features.RemoteHost)] public sealed class SolutionServiceTests { private static readonly TestComposition s_composition = FeaturesTestCompositions.Features.WithTestHostParts(TestHost.OutOfProcess); private static readonly TestComposition s_compositionWithFirstDocumentIsActiveAndVisible = s_composition.AddParts(typeof(FirstDocumentIsActiveAndVisibleDocumentTrackingService.Factory)); private static RemoteWorkspace CreateRemoteWorkspace() => new(FeaturesTestCompositions.RemoteHost.GetHostServices()); [Fact] public async Task TestCreation() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); } [Theory, CombinatorialData] public async Task TestGetSolutionWithPrimaryFlag(bool updatePrimaryBranch) { var code1 = @"class Test1 { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code1); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, cancellationToken: CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); Assert.Equal(WorkspaceKind.RemoteWorkspace, synched.WorkspaceKind); } [Fact] public async Task TestStrongNameProvider() { using var workspace = new AdhocWorkspace(); using var remoteWorkspace = CreateRemoteWorkspace(); var filePath = typeof(SolutionServiceTests).Assembly.Location; workspace.AddProject( ProjectInfo.Create( ProjectId.CreateNewId(), VersionStamp.Create(), "test", "test.dll", LanguageNames.CSharp, filePath: filePath, outputFilePath: filePath)); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, workspace.CurrentSolution); var solutionChecksum = await workspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None); var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); var compilationOptions = solution.Projects.First().CompilationOptions; Assert.IsType<DesktopStrongNameProvider>(compilationOptions.StrongNameProvider); Assert.IsType<XmlFileResolver>(compilationOptions.XmlReferenceResolver); var dirName = PathUtilities.GetDirectoryName(filePath); var array = new[] { dirName, dirName }; Assert.Equal(Hash.CombineValues(array, StringComparer.Ordinal), compilationOptions.StrongNameProvider.GetHashCode()); Assert.Equal(((XmlFileResolver)compilationOptions.XmlReferenceResolver).BaseDirectory, dirName); } [Fact] public async Task TestStrongNameProviderEmpty() { using var workspace = new AdhocWorkspace(); using var remoteWorkspace = CreateRemoteWorkspace(); var filePath = "testLocation"; workspace.AddProject( ProjectInfo.Create( ProjectId.CreateNewId(), VersionStamp.Create(), "test", "test.dll", LanguageNames.CSharp, filePath: filePath, outputFilePath: filePath)); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, workspace.CurrentSolution); var solutionChecksum = await workspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None); var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); var compilationOptions = solution.Projects.First().CompilationOptions; Assert.True(compilationOptions.StrongNameProvider is DesktopStrongNameProvider); Assert.True(compilationOptions.XmlReferenceResolver is XmlFileResolver); var array = new string[] { }; Assert.Equal(Hash.CombineValues(array, StringComparer.Ordinal), compilationOptions.StrongNameProvider.GetHashCode()); Assert.Null(((XmlFileResolver)compilationOptions.XmlReferenceResolver).BaseDirectory); } [Fact] public async Task TestCache() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var first = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); var second = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); // same instance from cache Assert.True(object.ReferenceEquals(first, second)); Assert.Equal(WorkspaceKind.RemoteWorkspace, first.WorkspaceKind); } [Fact] public async Task TestUpdatePrimaryWorkspace() { var code = @"class Test { void Method() { } }"; await VerifySolutionUpdate(code, s => s.WithDocumentText(s.Projects.First().DocumentIds.First(), SourceText.From(code + " "))); } [Fact] public async Task FallbackAnalyzerOptions() { using var workspace = TestWorkspace.CreateCSharp(""); static Solution SetSolutionProperties(Solution solution, int version) { return solution.WithFallbackAnalyzerOptions(ImmutableDictionary<string, StructuredAnalyzerConfigOptions>.Empty .Add(LanguageNames.CSharp, StructuredAnalyzerConfigOptions.Create(new DictionaryAnalyzerConfigOptions(ImmutableDictionary<string, string>.Empty .Add("cs_optionA", $"csA{version}") .Add("cs_optionB", $"csB{version}")))) .Add(LanguageNames.VisualBasic, StructuredAnalyzerConfigOptions.Create(new DictionaryAnalyzerConfigOptions(ImmutableDictionary<string, string>.Empty .Add("vb_optionA", $"vbA{version}") .Add("vb_optionB", $"vbB{version}")))) .Add(LanguageNames.FSharp, StructuredAnalyzerConfigOptions.Create(new DictionaryAnalyzerConfigOptions(ImmutableDictionary<string, string>.Empty .Add("fs_optionA", $"fsA{version}") .Add("fs_optionB", $"fsB{version}"))))); } static void ValidateProperties(Solution solution, int version, bool isRecovered) { // F# options are serialized because F# projects are not available OOP. AssertEx.SetEqual(isRecovered ? [ $"C#: [cs_optionA = csA{version}, cs_optionB = csB{version}]", $"Visual Basic: [vb_optionA = vbA{version}, vb_optionB = vbB{version}]", ] : [ $"F#: [fs_optionA = fsA{version}, fs_optionB = fsB{version}]", $"C#: [cs_optionA = csA{version}, cs_optionB = csB{version}]", $"Visual Basic: [vb_optionA = vbA{version}, vb_optionB = vbB{version}]", ], solution.FallbackAnalyzerOptions.Select(languageOptions => $"{languageOptions.Key}: [" + $"{string.Join(", ", languageOptions.Value.Keys.Order().Select(k => $"{k} = {(languageOptions.Value.TryGetValue(k, out var v) ? v : null)}"))}]")); } Assert.True(workspace.SetCurrentSolution(s => SetSolutionProperties(s, version: 0), WorkspaceChangeKind.SolutionChanged)); await VerifySolutionUpdate(workspace, newSolutionGetter: s => SetSolutionProperties(s, version: 1), oldSolutionValidator: s => ValidateProperties(s, version: 0, isRecovered: false), oldRecoveredSolutionValidator: s => ValidateProperties(s, version: 0, isRecovered: true), newRecoveredSolutionValidator: s => ValidateProperties(s, version: 1, isRecovered: true)).ConfigureAwait(false); } [Fact] public async Task ProjectCountsByLanguage() { using var workspace = TestWorkspace.CreateCSharp(""); static Solution SetSolutionProperties(Solution solution, int version) { foreach (var projectId in solution.ProjectIds) solution = solution.RemoveProject(projectId); for (var i = 0; i < version + 2; i++) solution = solution.AddProject("CS" + i, "CS" + i, LanguageNames.CSharp).Solution; return solution .AddProject("VB1", "VB1", LanguageNames.VisualBasic).Solution; } static void ValidateProperties(Solution solution, int version) { AssertEx.SetEqual( [ (LanguageNames.CSharp, version + 2), (LanguageNames.VisualBasic, 1), ], solution.SolutionState.ProjectCountByLanguage.Select(e => (e.Key, e.Value))); } Assert.True(workspace.SetCurrentSolution(s => SetSolutionProperties(s, version: 0), WorkspaceChangeKind.SolutionChanged)); await VerifySolutionUpdate(workspace, newSolutionGetter: s => SetSolutionProperties(s, version: 1), oldSolutionValidator: s => ValidateProperties(s, version: 0), newSolutionValidator: s => ValidateProperties(s, version: 1)).ConfigureAwait(false); } [Fact] public async Task ProjectProperties() { var dir = Path.GetDirectoryName(typeof(SolutionServiceTests).Assembly.Location); using var workspace = TestWorkspace.CreateCSharp(""); Solution SetProjectProperties(Solution solution, int version) { var projectId = solution.ProjectIds.Single(); return solution .WithProjectName(projectId, "Name" + version) .WithProjectAssemblyName(projectId, "AssemblyName" + version) .WithProjectFilePath(projectId, "FilePath" + version) .WithProjectOutputFilePath(projectId, "OutputFilePath" + version) .WithProjectOutputRefFilePath(projectId, "OutputRefFilePath" + version) .WithProjectCompilationOutputInfo(projectId, new CompilationOutputInfo("AssemblyPath" + version, dir + version)) .WithProjectDefaultNamespace(projectId, "DefaultNamespace" + version) .WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithm.Sha1 + version) .WithHasAllInformation(projectId, (version % 2) != 0) .WithRunAnalyzers(projectId, (version % 2) != 0); } void ValidateProperties(Solution solution, int version) { var project = solution.Projects.Single(); Assert.Equal("Name" + version, project.Name); Assert.Equal("AssemblyName" + version, project.AssemblyName); Assert.Equal("FilePath" + version, project.FilePath); Assert.Equal("OutputFilePath" + version, project.OutputFilePath); Assert.Equal("OutputRefFilePath" + version, project.OutputRefFilePath); Assert.Equal(dir + version, project.CompilationOutputInfo.GeneratedFilesOutputDirectory); Assert.Equal("AssemblyPath" + version, project.CompilationOutputInfo.AssemblyPath); Assert.Equal("DefaultNamespace" + version, project.DefaultNamespace); Assert.Equal(SourceHashAlgorithm.Sha1 + version, project.State.ChecksumAlgorithm); Assert.Equal((version % 2) != 0, project.State.HasAllInformation); Assert.Equal((version % 2) != 0, project.State.RunAnalyzers); } Assert.True(workspace.SetCurrentSolution(s => SetProjectProperties(s, version: 0), WorkspaceChangeKind.SolutionChanged)); await VerifySolutionUpdate(workspace, newSolutionGetter: s => SetProjectProperties(s, version: 1), oldSolutionValidator: s => ValidateProperties(s, version: 0), newSolutionValidator: s => ValidateProperties(s, version: 1)).ConfigureAwait(false); } [Fact] public Task TestUpdateDocumentInfo() => VerifySolutionUpdate(@"class Test { void Method() { } }", s => s.WithDocumentFolders(s.Projects.First().Documents.First().Id, ["test"])); [Fact] public Task TestAddUpdateRemoveProjects() => VerifySolutionUpdate(@"class Test { void Method() { } }", s => { var existingProjectId = s.ProjectIds.First(); s = s.AddProject("newProject", "newProject", LanguageNames.CSharp).Solution; var project = s.GetProject(existingProjectId); project = project.WithCompilationOptions(project.CompilationOptions.WithModuleName("modified")); var existingDocumentId = project.DocumentIds.First(); project = project.AddDocument("newDocument", SourceText.From("// new text")).Project; var document = project.GetDocument(existingDocumentId); document = document.WithSourceCodeKind(SourceCodeKind.Script); return document.Project.Solution; }); [Fact] public async Task TestAdditionalDocument() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); var projectId = workspace.CurrentSolution.ProjectIds.First(); var additionalDocumentId = DocumentId.CreateNewId(projectId); var additionalDocumentInfo = DocumentInfo.Create( additionalDocumentId, "additionalFile", loader: TextLoader.From(TextAndVersion.Create(SourceText.From("test"), VersionStamp.Create()))); await VerifySolutionUpdate(workspace, s => { return s.AddAdditionalDocument(additionalDocumentInfo); }); workspace.OnAdditionalDocumentAdded(additionalDocumentInfo); await VerifySolutionUpdate(workspace, s => { return s.WithAdditionalDocumentText(additionalDocumentId, SourceText.From("changed")); }); await VerifySolutionUpdate(workspace, s => { return s.RemoveAdditionalDocument(additionalDocumentId); }); } [Fact] public async Task TestAnalyzerConfigDocument() { var configPath = Path.Combine(Path.GetTempPath(), ".editorconfig"); var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); var projectId = workspace.CurrentSolution.ProjectIds.First(); var analyzerConfigDocumentId = DocumentId.CreateNewId(projectId); var analyzerConfigDocumentInfo = DocumentInfo.Create( analyzerConfigDocumentId, name: ".editorconfig", loader: TextLoader.From(TextAndVersion.Create(SourceText.From("root = true"), VersionStamp.Create(), filePath: configPath)), filePath: configPath); await VerifySolutionUpdate(workspace, s => { return s.AddAnalyzerConfigDocuments([analyzerConfigDocumentInfo]); }); workspace.OnAnalyzerConfigDocumentAdded(analyzerConfigDocumentInfo); await VerifySolutionUpdate(workspace, s => { return s.WithAnalyzerConfigDocumentText(analyzerConfigDocumentId, SourceText.From("root = false")); }); await VerifySolutionUpdate(workspace, s => { return s.RemoveAnalyzerConfigDocument(analyzerConfigDocumentId); }); } [Fact, Trait(Traits.Feature, Traits.Features.RemoteHost)] public async Task TestDocument() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); var projectId = workspace.CurrentSolution.ProjectIds.First(); var documentId = DocumentId.CreateNewId(projectId); var documentInfo = DocumentInfo.Create( documentId, "sourceFile", loader: TextLoader.From(TextAndVersion.Create(SourceText.From("class A { }"), VersionStamp.Create()))); await VerifySolutionUpdate(workspace, s => { return s.AddDocument(documentInfo); }); workspace.OnDocumentAdded(documentInfo); await VerifySolutionUpdate(workspace, s => { return s.WithDocumentText(documentId, SourceText.From("class Changed { }")); }); await VerifySolutionUpdate(workspace, s => { return s.RemoveDocument(documentId); }); } [Fact] public async Task TestRemoteWorkspace() { var code = @"class Test { void Method() { } }"; // create base solution using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); // create solution service var solution1 = workspace.CurrentSolution; var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution1); var remoteSolution1 = await GetInitialOOPSolutionAsync(remoteWorkspace, assetProvider, solution1); await Verify(remoteWorkspace, solution1, remoteSolution1); // update remote workspace var currentSolution = remoteSolution1.WithDocumentText(remoteSolution1.Projects.First().Documents.First().Id, SourceText.From(code + " class Test2 { }")); var oopSolution2 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(currentSolution); await Verify(remoteWorkspace, currentSolution, oopSolution2); // move backward await Verify(remoteWorkspace, remoteSolution1, await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(remoteSolution1)); // move forward currentSolution = oopSolution2.WithDocumentText(oopSolution2.Projects.First().Documents.First().Id, SourceText.From(code + " class Test3 { }")); var remoteSolution3 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(currentSolution); await Verify(remoteWorkspace, currentSolution, remoteSolution3); // move to new solution backward var solutionInfo2 = await assetProvider.CreateSolutionInfoAsync( await solution1.CompilationState.GetChecksumAsync(CancellationToken.None), remoteWorkspace.Services.SolutionServices, CancellationToken.None); var solution2 = remoteWorkspace.GetTestAccessor().CreateSolutionFromInfo(solutionInfo2); // move to new solution forward var solution3 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(solution2); Assert.NotNull(solution3); await Verify(remoteWorkspace, solution1, solution3); static async Task<Solution> GetInitialOOPSolutionAsync(RemoteWorkspace remoteWorkspace, AssetProvider assetProvider, Solution solution) { // set up initial solution var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); // get solution in remote host return await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); } static async Task Verify(RemoteWorkspace remoteWorkspace, Solution givenSolution, Solution remoteSolution) { // verify we got solution expected Assert.Equal(await givenSolution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // verify remote workspace got updated Assert.Equal(remoteSolution, remoteWorkspace.CurrentSolution); } } [Theory, CombinatorialData] [WorkItem("https://github.com/dotnet/roslyn/issues/48564")] public async Task TestAddingProjectsWithExplicitOptions(bool useDefaultOptionValue) { using var workspace = TestWorkspace.CreateCSharp(@"public class C { }"); var solution = workspace.CurrentSolution; var solutionChecksum1 = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); // legacy options are not serialized and have no effect on checksum: var newOptionValue = useDefaultOptionValue ? FormattingOptions2.NewLine.DefaultValue : FormattingOptions2.NewLine.DefaultValue + FormattingOptions2.NewLine.DefaultValue; solution = solution.WithOptions(solution.Options .WithChangedOption(FormattingOptions.NewLine, LanguageNames.CSharp, newOptionValue) .WithChangedOption(FormattingOptions.NewLine, LanguageNames.VisualBasic, newOptionValue)); var solutionChecksum2 = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); Assert.Equal(solutionChecksum1, solutionChecksum2); } [Fact] public async Task TestFrozenSourceGeneratedDocument() { using var workspace = TestWorkspace.CreateCSharp(@""); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution .Projects.Single() .AddAnalyzerReference(new AnalyzerFileReference(typeof(Microsoft.CodeAnalysis.TestSourceGenerator.HelloWorldGenerator).Assembly.Location, new TestAnalyzerAssemblyLoader())) .Solution; // First sync the solution over that has a generator var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); // Now freeze with some content var documentIdentity = (await solution.Projects.Single().GetSourceGeneratedDocumentsAsync()).First().Identity; var frozenText1 = SourceText.From("// Hello, World!"); var frozenSolution1 = solution.WithFrozenSourceGeneratedDocument(documentIdentity, DateTime.Now, frozenText1).Project.Solution; assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, frozenSolution1); solutionChecksum = await frozenSolution1.CompilationState.GetChecksumAsync(CancellationToken.None); synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); // Try freezing with some different content from the original solution var frozenText2 = SourceText.From("// Hello, World! A second time!"); var frozenSolution2 = solution.WithFrozenSourceGeneratedDocument(documentIdentity, DateTime.Now, frozenText2).Project.Solution; assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, frozenSolution2); solutionChecksum = await frozenSolution2.CompilationState.GetChecksumAsync(CancellationToken.None); synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); } [Fact] public async Task TestPartialProjectSync_GetSolutionFirst() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); solution = project2.Solution; var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // Do the initial full sync await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); Assert.Equal(solutionChecksum, await syncedFullSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); Assert.Equal(2, syncedFullSolution.Projects.Count()); // Syncing project1 should do nothing as syncing the solution already synced it over. var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project1SyncedSolution.Projects.Count()); // Syncing project2 should do nothing as syncing the solution already synced it over. var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); } [Fact] public async Task TestPartialProjectSync_GetSolutionLast() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); solution = project2.Solution; var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // Syncing project 1 should just since it over. await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(1, project1SyncedSolution.Projects.Count()); Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); // Syncing project 2 will have both projects since we reused the prior snapshot await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); // then syncing the whole solution should still contain both await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); } [Fact] public async Task TestPartialProjectSync_GetDependentProjects1() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); solution = project3.Solution.AddProjectReference(project3.Id, new(project3.Solution.Projects.Single(p => p.Name == "P2").Id)); var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(1, project2SyncedSolution.Projects.Count()); Assert.Equal(project2.Name, project2SyncedSolution.Projects.Single().Name); // syncing project 3 should sync project 2 as well because of the p2p ref await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project3SyncedSolution.Projects.Count()); } [Fact] public async Task TestPartialProjectSync_GetDependentProjects2() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); solution = project3.Solution.AddProjectReference(project3.Id, new(project3.Solution.Projects.Single(p => p.Name == "P2").Id)); var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // syncing P3 should since project P2 as well because of the p2p ref await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project3SyncedSolution.Projects.Count()); // if we then sync just P2, we will reuse our snapshot that contains P3 await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); // if we then sync just P1, we will reuse our prior snapshots with P2 and P3, so all projects will be present now await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] public async Task TestPartialProjectSync_GetDependentProjects2_ReverseOrder() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); solution = project3.Solution.AddProjectReference(project3.Id, new(project3.Solution.Projects.Single(p => p.Name == "P2").Id)); var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // syncing P1 should contain just P1 await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(1, project3SyncedSolution.Projects.Count()); // if we then sync just P2, we will get both P1 and P2 -- P1, because we reused the snapshot await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); // if we then sync just P3, we will reuse our prior snapshots with P1 and P2, so all projects will be present now await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] public async Task TestPartialProjectSync_GetDependentProjects3() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) .AddProjectReference(project2.Id, new(project1.Id)); var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // syncing project3 should since project2 and project1 as well because of the p2p ref await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project3SyncedSolution.Projects.Count()); // syncing project2 will still have all projects since we're reusing the prior snapshot await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project2SyncedSolution.Projects.Count()); // syncing project1 is the same: all projects should be present await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] public async Task TestPartialProjectSync_GetDependentProjects3_ReverseOrder() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) .AddProjectReference(project2.Id, new(project1.Id)); var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // syncing project1 will contain just P1 await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(1, project3SyncedSolution.Projects.Count()); // syncing project2 will contain both await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); // syncing project3 gives everything await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] public async Task TestPartialProjectSync_GetDependentProjects4() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) .AddProjectReference(project3.Id, new(project1.Id)); var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // syncing project3 should since project2 and project1 as well because of the p2p ref await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project3SyncedSolution.Projects.Count()); // Syncing project2 will still contain all projects since we're reusing the prior snapshot await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project2SyncedSolution.Projects.Count()); // Syncing project1 will still contain all projects since we're reusing the prior snapshot await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] public async Task TestPartialProjectSync_DoesNotSeeChangesOutsideOfCone() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.VisualBasic); solution = project2.Solution; var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // Do the initial full sync await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); Assert.Equal(2, fullSyncedSolution.Projects.Count()); // Mutate both projects to each have a document in it. solution = solution.GetProject(project1.Id).AddDocument("X.cs", SourceText.From("// X")).Project.Solution; solution = solution.GetProject(project2.Id).AddDocument("Y.vb", SourceText.From("' Y")).Project.Solution; // Now just sync project1's cone over. We should not see the change to project2 on the remote side. // But we will still see project2. { await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project1SyncedSolution.Projects.Count()); var csharpProject = project1SyncedSolution.Projects.Single(p => p.Language == LanguageNames.CSharp); var vbProject = project1SyncedSolution.Projects.Single(p => p.Language == LanguageNames.VisualBasic); Assert.True(csharpProject.DocumentIds.Count == 2); Assert.Empty(vbProject.DocumentIds); } // Similarly, if we sync just project2's cone over: { await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); var csharpProject = project2SyncedSolution.Projects.Single(p => p.Language == LanguageNames.CSharp); var vbProject = project2SyncedSolution.Projects.Single(p => p.Language == LanguageNames.VisualBasic); Assert.Single(csharpProject.DocumentIds); Assert.Single(vbProject.DocumentIds); } } [Fact] public async Task TestPartialProjectSync_AddP2PRef() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); solution = project2.Solution; var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // Do the initial full sync await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); Assert.Equal(2, fullSyncedSolution.Projects.Count()); // Mutate both projects to have a document in it, and add a p2p ref from project1 to project2 solution = solution.GetProject(project1.Id).AddDocument("X.cs", SourceText.From("// X")).Project.Solution; solution = solution.GetProject(project2.Id).AddDocument("Y.cs", SourceText.From("// Y")).Project.Solution; solution = solution.GetProject(project1.Id).AddProjectReference(new ProjectReference(project2.Id)).Solution; // Now just sync project1's cone over. This will validate that the p2p ref doesn't try to add a new // project, but instead sees the existing one. { await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project1SyncedSolution.Projects.Count()); var project1Synced = project1SyncedSolution.GetRequiredProject(project1.Id); var project2Synced = project1SyncedSolution.GetRequiredProject(project2.Id); Assert.True(project1Synced.DocumentIds.Count == 2); Assert.Single(project2Synced.DocumentIds); Assert.Single(project1Synced.ProjectReferences); } } [Fact] public async Task TestPartialProjectSync_ReferenceToNonExistentProject() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); // This reference a project that doesn't exist. // Ensure that it's still fine to get the checksum for this project we have. project1 = project1.AddProjectReference(new ProjectReference(ProjectId.CreateNewId())); solution = project1.Solution; var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); } [Fact] public async Task TestPartialProjectSync_SourceGeneratorExecutionVersion_1() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); solution = project2.Solution; var map = new Dictionary<Checksum, object>(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService<ISerializerService>(), map), remoteWorkspace.Services.SolutionServices); // Do the initial full sync await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); Assert.Equal(2, fullSyncedSolution.Projects.Count()); // Update the source generator versions for all projects for the local workspace. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: true); await GetWorkspaceWaiter(workspace).ExpeditedWaitAsync(); solution = workspace.CurrentSolution; // Now just sync project1's cone over. This will validate that that we get the right checksums, even with a // partial cone sync. { await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); } } private static IAsynchronousOperationWaiter GetWorkspaceWaiter(TestWorkspace workspace) { var operations = workspace.ExportProvider.GetExportedValue<AsynchronousOperationListenerProvider>(); return operations.GetWaiter(FeatureAttribute.Workspace); } [Fact] public void TestNoActiveDocumentSemanticModelNotCached() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var document1 = project1.Documents.Single(); // Without anything holding onto the semantic model, it should get releases. var objectReference = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference.AssertReleased(); } [Fact] public void TestActiveDocumentSemanticModelCached() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithFirstDocumentIsActiveAndVisible); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var document1 = project1.Documents.Single(); // Since this is the active document, we should hold onto it. var objectReference = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference.AssertHeld(); } [Fact] public void TestOnlyActiveDocumentSemanticModelCached() { using var workspace = TestWorkspace.Create(""" <Workspace> <Project Language="C#" AssemblyName="Assembly1" CommonReferences="true"> <Document FilePath="File1.cs"> class Program1 { } </Document> <Document FilePath="File2.cs"> class Program2 { } </Document> </Project> </Workspace> """, composition: s_compositionWithFirstDocumentIsActiveAndVisible); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var document1 = project1.Documents.First(); var document2 = project1.Documents.Last(); // Only the semantic model for the active document should be cached. var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); var objectReference2 = ObjectReference.CreateFromFactory(() => document2.GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference1.AssertHeld(); objectReference2.AssertReleased(); } [Fact] public void TestActiveAndRelatedDocumentSemanticModelCached() { using var workspace = TestWorkspace.Create(""" <Workspace> <Project Language="C#" AssemblyName="Assembly1" CommonReferences="true"> <Document FilePath="File1.cs"> class Program1 { } </Document> <Document FilePath="File2.cs"> class Program2 { } </Document> </Project> <Project Language="C#" AssemblyName="Assembly2" CommonReferences="true"> <Document IsLinkFile="true" LinkAssemblyName="Assembly1" LinkFilePath="File1.cs" /> </Project> </Workspace> """, composition: s_compositionWithFirstDocumentIsActiveAndVisible); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(p => p.AssemblyName == "Assembly1"); var project2 = solution.Projects.Single(p => p.AssemblyName == "Assembly2"); var document1 = project1.Documents.First(); var document2 = project1.Documents.Last(); var document3 = project2.Documents.Single(); // Only the semantic model for the active document should be cached. var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); var objectReference2 = ObjectReference.CreateFromFactory(() => document2.GetSemanticModelAsync().GetAwaiter().GetResult()); var objectReference3 = ObjectReference.CreateFromFactory(() => document3.GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference1.AssertHeld(); objectReference2.AssertReleased(); objectReference3.AssertHeld(); } [Fact] public async Task TestRemoteWorkspaceCachesNothingIfActiveDocumentNotSynced() { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithFirstDocumentIsActiveAndVisible); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var document1 = project1.Documents.Single(); // Locally the semantic model will be held var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference1.AssertHeld(); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var syncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); // The remote semantic model will not be held as it doesn't know what the active document is yet. var objectReference2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference2.AssertReleased(); } [Theory, CombinatorialData] public async Task TestRemoteWorkspaceCachesPropertyIfActiveDocumentIsSynced(bool updatePrimaryBranch) { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithFirstDocumentIsActiveAndVisible); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var document1 = project1.Documents.Single(); // Locally the semantic model will be held var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference1.AssertHeld(); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var remoteDocumentTrackingService = (RemoteDocumentTrackingService)remoteWorkspace.Services.GetRequiredService<IDocumentTrackingService>(); remoteDocumentTrackingService.SetActiveDocument(document1.Id); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var syncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, CancellationToken.None); // The remote semantic model will be held as it refers to the active document. var objectReference2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference2.AssertHeld(); } [Theory, CombinatorialData] public async Task ValidateUpdaterInformsRemoteWorkspaceOfActiveDocument(bool updatePrimaryBranch) { var code = @"class Test { void Method() { } }"; using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithFirstDocumentIsActiveAndVisible); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var document1 = project1.Documents.Single(); // Locally the semantic model will be held var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference1.AssertHeld(); // By creating a checksum updater, we should notify the remote workspace of the active document. var listenerProvider = workspace.ExportProvider.GetExportedValue<AsynchronousOperationListenerProvider>(); using var checksumUpdater = new SolutionChecksumUpdater(workspace, listenerProvider, CancellationToken.None); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var syncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, CancellationToken.None); var waiter = listenerProvider.GetWaiter(FeatureAttribute.SolutionChecksumUpdater); await waiter.ExpeditedWaitAsync(); // The remote semantic model will be held as it refers to the active document. var objectReference2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference2.AssertHeld(); } [Theory, CombinatorialData] public async Task ValidateUpdaterInformsRemoteWorkspaceOfActiveDocument_EvenAcrossActiveDocumentChanges(bool updatePrimaryBranch) { using var workspace = TestWorkspace.Create(""" <Workspace> <Project Language="C#" AssemblyName="Assembly1" CommonReferences="true"> <Document FilePath="File1.cs"> class Program1 { } </Document> <Document FilePath="File2.cs"> class Program2 { } </Document> </Project> </Workspace> """, composition: s_composition.AddParts(typeof(TestDocumentTrackingService))); using var remoteWorkspace = CreateRemoteWorkspace(); var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); var document1 = project1.Documents.First(); var document2 = project1.Documents.Last(); // By creating a checksum updater, we should notify the remote workspace of the active document. Have it // initially be set to the first document. var documentTrackingService = (TestDocumentTrackingService)workspace.Services.GetRequiredService<IDocumentTrackingService>(); documentTrackingService.SetActiveDocument(document1.Id); // Locally the semantic model for the first document will be held, but the second will not. var objectReference1_step1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); var objectReference2_step1 = ObjectReference.CreateFromFactory(() => document2.GetSemanticModelAsync().GetAwaiter().GetResult()); objectReference1_step1.AssertHeld(); objectReference2_step1.AssertReleased(); var listenerProvider = workspace.ExportProvider.GetExportedValue<AsynchronousOperationListenerProvider>(); using var checksumUpdater = new SolutionChecksumUpdater(workspace, listenerProvider, CancellationToken.None); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var syncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, CancellationToken.None); var waiter = listenerProvider.GetWaiter(FeatureAttribute.SolutionChecksumUpdater); await waiter.ExpeditedWaitAsync(); // The remote semantic model should match the local behavior once it has been notified that the first document is active. var oopDocumentReference1_step1 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); var oopDocumentReference2_step1 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document2.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); oopDocumentReference1_step1.AssertHeld(); oopDocumentReference2_step1.AssertReleased(); // Now, change the active document to the second document. documentTrackingService.SetActiveDocument(document2.Id); // And get the semantic models again. The second document should now be held, and the first released. var objectReference1_step2 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); var objectReference2_step2 = ObjectReference.CreateFromFactory(() => document2.GetSemanticModelAsync().GetAwaiter().GetResult()); // The second document should be held. objectReference2_step2.AssertHeld(); // Ensure that the active doc change is sync'ed to oop. await waiter.ExpeditedWaitAsync(); // And get the semantic models again on the oop side. The second document should now be held, and the first released. var oopDocumentReference1_step2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); var oopDocumentReference2_step2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document2.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); // The second document on oop should now be held. oopDocumentReference2_step2.AssertHeld(); } private static async Task VerifySolutionUpdate(string code, Func<Solution, Solution> newSolutionGetter) { using var workspace = TestWorkspace.CreateCSharp(code); await VerifySolutionUpdate(workspace, newSolutionGetter); } #nullable enable private static Task VerifySolutionUpdate( TestWorkspace workspace, Func<Solution, Solution> newSolutionGetter, Action<Solution>? oldSolutionValidator = null, Action<Solution>? newSolutionValidator = null) => VerifySolutionUpdate(workspace, newSolutionGetter, oldSolutionValidator, oldSolutionValidator, newSolutionValidator); private static async Task VerifySolutionUpdate( TestWorkspace workspace, Func<Solution, Solution> newSolutionGetter, Action<Solution>? oldSolutionValidator, Action<Solution>? oldRecoveredSolutionValidator, Action<Solution>? newRecoveredSolutionValidator) { var solution = workspace.CurrentSolution; oldSolutionValidator?.Invoke(solution); var map = new Dictionary<Checksum, object>(); using var remoteWorkspace = CreateRemoteWorkspace(); var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution, map); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); // update primary workspace await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); var recoveredSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); oldRecoveredSolutionValidator?.Invoke(recoveredSolution); Assert.Equal(WorkspaceKind.RemoteWorkspace, recoveredSolution.WorkspaceKind); Assert.Equal(solutionChecksum, await recoveredSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // get new solution var newSolution = newSolutionGetter(solution); var newSolutionChecksum = await newSolution.CompilationState.GetChecksumAsync(CancellationToken.None); await newSolution.AppendAssetMapAsync(map, CancellationToken.None); // get solution without updating primary workspace var recoveredNewSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(newSolutionChecksum, await recoveredNewSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // do same once updating primary workspace await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, newSolutionChecksum, CancellationToken.None); var third = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(newSolutionChecksum, await third.CompilationState.GetChecksumAsync(CancellationToken.None)); newRecoveredSolutionValidator?.Invoke(recoveredNewSolution); } private static async Task<AssetProvider> GetAssetProviderAsync(Workspace workspace, RemoteWorkspace remoteWorkspace, Solution solution, Dictionary<Checksum, object>? map = null) { // make sure checksum is calculated await solution.CompilationState.GetChecksumAsync(CancellationToken.None); map ??= []; await solution.AppendAssetMapAsync(map, CancellationToken.None); var sessionId = Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())); var storage = new SolutionAssetCache(); var assetSource = new SimpleAssetSource(workspace.Services.GetRequiredService<ISerializerService>(), map); return new AssetProvider(sessionId, storage, assetSource, remoteWorkspace.Services.SolutionServices); } }