/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs
2 199 строк
106 KB
Cyrus Najmabadi
Only features
30 дек 2025, 06:45
30 дек 2025, 06:45
4a47b43
Код
Авторство
О чём код?
// 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.Composition; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Copilot; using Microsoft.CodeAnalysis.DesignerAttribute; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; 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 Microsoft.CodeAnalysis.UnitTests; using Microsoft.CodeAnalysis.UnitTests.Logging; using Microsoft.VisualStudio.Threading; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; using Roslyn.Utilities; using Xunit; namespace Roslyn.VisualStudio.Next.UnitTests.Remote; [UseExportProvider] [Trait(Traits.Feature, Traits.Features.RemoteHost)] public sealed partial class ServiceHubServicesTests { private static TestWorkspace CreateWorkspace( Type[] additionalParts = null, Type[] additionalRemoteParts = null) { var workspace = new TestWorkspace( composition: FeaturesTestCompositions.Features.WithTestHostParts(TestHost.OutOfProcess) .AddParts(additionalParts) .RemoveParts([typeof(Roslyn.Test.Utilities.TestWorkspaceConfigurationService)])); // We use a different implementation of the WorkspaceConfigurationService if (additionalRemoteParts != null) { var clientProvider = (InProcRemoteHostClientProvider)workspace.Services.GetService<IRemoteHostClientProvider>(); clientProvider.AdditionalRemoteParts = additionalRemoteParts; } return workspace; } [Fact] public async Task TestRemoteHostSynchronize() { var code = @"class Test { void Method() { } }"; using var workspace = CreateWorkspace(); workspace.InitializeDocuments(LanguageNames.CSharp, files: [code], openDocuments: false); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); await VerifyAssetStorageAsync(client, solution); var remoteWorkpace = client.GetRemoteWorkspace(); Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkpace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); } [Fact] public async Task TestRemoteHostTextSynchronize() { var code = @"class Test { void Method() { } }"; using var workspace = CreateWorkspace(); workspace.InitializeDocuments(LanguageNames.CSharp, files: [code], openDocuments: false); var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var solution = workspace.CurrentSolution; // sync base solution await UpdatePrimaryWorkspace(client, solution); await VerifyAssetStorageAsync(client, solution); // get basic info var oldDocument = solution.Projects.First().Documents.First(); var oldState = await oldDocument.State.GetStateChecksumsAsync(CancellationToken.None); var oldText = await oldDocument.GetTextAsync(); // update text var newText = oldText.WithChanges(new TextChange(TextSpan.FromBounds(0, 0), "/* test */")); // apply change to solution var newDocument = oldDocument.WithText(newText); var newState = await newDocument.State.GetStateChecksumsAsync(CancellationToken.None); // sync await client.TryInvokeAsync<IRemoteAssetSynchronizationService>( (service, cancellationToken) => service.SynchronizeTextChangesAsync(oldDocument.Id, oldState.Text, newText.GetTextChanges(oldText).AsImmutable(), newState.Text, cancellationToken), CancellationToken.None); // check that text already exist in remote side Assert.True(client.TestData.WorkspaceManager.SolutionAssetCache.TryGetAsset<SerializableSourceText>(newState.Text, out var serializableRemoteText)); Assert.Equal(newText.ToString(), (await serializableRemoteText.GetTextAsync(CancellationToken.None)).ToString()); } private static async Task<AssetProvider> GetAssetProviderAsync(Workspace workspace, Workspace 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.GetService<ISerializerService>(), map); return new AssetProvider(sessionId, storage, assetSource, remoteWorkspace.Services.SolutionServices); } [Fact] public async Task TestDesignerAttributes() { var source = @"[System.ComponentModel.DesignerCategory(""Form"")] class Test { }"; using var workspace = CreateWorkspace(); workspace.InitializeDocuments(LanguageNames.CSharp, files: [source], openDocuments: false); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var remoteWorkspace = client.GetRemoteWorkspace(); var cancellationTokenSource = new CancellationTokenSource(); var solution = workspace.CurrentSolution; // Ensure remote workspace is in sync with normal workspace. var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); var callback = new DesignerAttributeComputerCallback(); using var connection = client.CreateConnection<IRemoteDesignerAttributeDiscoveryService>(callback); // Actually pass in the document to scan. Otherwise the test takes several seconds waiting to do the slow // sweep of the entire solution. var priorityDocumentId = solution.Projects.Single().Documents.Single().Id; var invokeTask = connection.TryInvokeAsync( solution, (service, checksum, callbackId, cancellationToken) => service.DiscoverDesignerAttributesAsync( callbackId, checksum, priorityDocumentId, cancellationToken), cancellationTokenSource.Token); var infos = await callback.Infos; Assert.Equal(1, infos.Length); var info = infos[0]; Assert.Equal("Form", info.Category); Assert.Equal(solution.Projects.Single().Documents.Single().Id, info.DocumentId); // Let the discovery know it can stop processing. cancellationTokenSource.Cancel(); try { await invokeTask; } catch (OperationCanceledException) { } } [Fact] public async Task TestDesignerAttributesUnsupportedLanguage() { var source = @"// TS code"; using var workspace = CreateWorkspace(); workspace.InitializeDocuments("TypeScript", files: [source], openDocuments: true); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var remoteWorkspace = client.GetRemoteWorkspace(); var cancellationTokenSource = new CancellationTokenSource(); var solution = workspace.CurrentSolution; // Ensure remote workspace is in sync with normal workspace. var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); var callback = new DesignerAttributeComputerCallback(); var listenerProvider = workspace.ExportProvider.GetExportedValue<IAsynchronousOperationListenerProvider>(); await DesignerAttributeDiscoveryService.DiscoverDesignerAttributesAsync( workspace.CurrentSolution, workspace.CurrentSolution.Projects.Single().Documents.Single(), client, listenerProvider.GetListener(FeatureAttribute.DesignerAttributes), callback, CancellationToken.None); } private sealed class DesignerAttributeComputerCallback : IDesignerAttributeDiscoveryService.ICallback { private readonly TaskCompletionSource<ImmutableArray<DesignerAttributeData>> _infosSource = new(); public Task<ImmutableArray<DesignerAttributeData>> Infos => _infosSource.Task; public ValueTask ReportDesignerAttributeDataAsync(ImmutableArray<DesignerAttributeData> infos, CancellationToken cancellationToken) { _infosSource.SetResult(infos); return ValueTask.CompletedTask; } } [Fact] public async Task TestUnknownProject() { using var workspace = CreateWorkspace([typeof(NoCompilationLanguageService)]); var solution = workspace.CurrentSolution.AddProject("unknown", "unknown", NoCompilationConstants.LanguageName).Solution; using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var remoteWorkspace = client.GetRemoteWorkspace(); await UpdatePrimaryWorkspace(client, solution); await VerifyAssetStorageAsync(client, solution); // Only C# and VB projects are supported in Remote workspace. // See "RemoteSupportedLanguages.IsSupported" Assert.Empty(remoteWorkspace.CurrentSolution.Projects); // No serializable remote options affect options checksum, so the checksums should match. Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); solution = solution.RemoveProject(solution.ProjectIds.Single()); Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); } [Theory, CombinatorialData] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1365014")] public async Task TestRemoteHostSynchronizeIncrementalUpdate(bool applyInBatch) { using var workspace = CreateWorkspace(); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var remoteWorkspace = client.GetRemoteWorkspace(); // verify initial setup workspace.SetCurrentSolution(Populate, WorkspaceChangeKind.SolutionChanged); var solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); await VerifyAssetStorageAsync(client, solution); Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // incrementally update solution = await VerifyIncrementalUpdatesAsync( workspace, remoteWorkspace, client, solution, applyInBatch, csAddition: " ", vbAddition: " "); Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // incrementally update solution = await VerifyIncrementalUpdatesAsync( workspace, remoteWorkspace, client, solution, applyInBatch, csAddition: "\r\nclass Addition { }", vbAddition: "\r\nClass VB\r\nEnd Class"); Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/52578")] public async Task TestIncrementalUpdateHandlesReferenceReversal() { using var workspace = CreateWorkspace(); ProjectId projectId1 = null; ProjectId projectId2 = null; workspace.SetCurrentSolution(solution => { solution = AddProject(solution, LanguageNames.CSharp, documents: [], additionalDocuments: [], p2pReferences: []); solution = AddProject(solution, LanguageNames.CSharp, documents: [], additionalDocuments: [], p2pReferences: []); projectId1 = solution.Projects.Single(p => p.Name == "Project0").Id; projectId2 = solution.Projects.Single(p => p.Name == "Project1").Id; // Start with projectId1 -> projectId2 return solution.AddProjectReference(projectId1, new ProjectReference(projectId2)); }, WorkspaceChangeKind.SolutionChanged); var solution = workspace.CurrentSolution; using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var remoteWorkspace = client.GetRemoteWorkspace(); // verify initial setup await UpdatePrimaryWorkspace(client, solution); await VerifyAssetStorageAsync(client, solution); Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // reverse project references and incrementally update workspace.SetCurrentSolution(solution => { solution = solution.RemoveProjectReference(projectId1, new ProjectReference(projectId2)); return solution.AddProjectReference(projectId2, new ProjectReference(projectId1)); }, WorkspaceChangeKind.SolutionChanged); solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); // reverse project references again and incrementally update workspace.SetCurrentSolution(solution => { solution = solution.RemoveProjectReference(projectId2, new ProjectReference(projectId1)); return solution.AddProjectReference(projectId1, new ProjectReference(projectId2)); }, WorkspaceChangeKind.SolutionChanged); solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); } [Fact] public async Task TestRemoteWorkspaceCircularReferences() { using var tempRoot = new TempRoot(); var file = tempRoot.CreateDirectory().CreateFile("p1.dll"); file.CopyContentFrom(typeof(object).Assembly.Location); var p1 = ProjectId.CreateNewId(); var p2 = ProjectId.CreateNewId(); var solutionInfo = SolutionInfo.Create( SolutionId.CreateNewId(), VersionStamp.Create(), "", [ ProjectInfo.Create( p1, VersionStamp.Create(), "p1", "p1", LanguageNames.CSharp, outputFilePath: file.Path, projectReferences: [new ProjectReference(p2)]), ProjectInfo.Create( p2, VersionStamp.Create(), "p2", "p2", LanguageNames.CSharp, metadataReferences: [MetadataReference.CreateFromFile(file.Path)]) ]); using var remoteWorkspace = new RemoteWorkspace(FeaturesTestCompositions.RemoteHost.GetHostServices()); // this shouldn't throw exception var solution = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync( remoteWorkspace.GetTestAccessor().CreateSolutionFromInfo(solutionInfo)); Assert.NotNull(solution); } private static ImmutableArray<ImmutableArray<T>> Permute<T>(T[] values) { using var _ = ArrayBuilder<ImmutableArray<T>>.GetInstance(out var result); DoPermute(0, values.Length - 1); return result.ToImmutableAndClear(); void DoPermute(int start, int end) { if (start == end) { // We have one of our possible n! solutions, // add it to the list. result.Add([.. values]); } else { for (var i = start; i <= end; i++) { (values[start], values[i]) = (values[i], values[start]); DoPermute(start + 1, end); (values[start], values[i]) = (values[i], values[start]); } } } } private static async Task TestInProcAndRemoteWorkspace( params ImmutableArray<(string hintName, SourceText text)>[] values) { // Try every permutation of these values. foreach (var permutation in Permute(values)) await TestInProcAndRemoteWorkspaceWorker(permutation); } private static async Task TestInProcAndRemoteWorkspaceWorker( ImmutableArray<ImmutableArray<(string hintName, SourceText text)>> values) { var throwIfCalled = false; ImmutableArray<(string hintName, SourceText text)> sourceTexts = default; var generator = new CallbackGenerator( onInit: _ => { }, onExecute: _ => { }, computeSourceTexts: () => { Contract.ThrowIfTrue(throwIfCalled); return sourceTexts; }); using var localWorkspace = CreateWorkspace(); DocumentId tempDocId; // Keep this all in a nested scope so we don't accidentally access this data inside the loop below. We only // want to access the true workspace solution (which will be a fork of the solution we're producing here). { var projectId = ProjectId.CreateNewId(); var analyzerReference = new TestGeneratorReference(generator); var project = localWorkspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId) .WithCompilationOutputInfo(new CompilationOutputInfo( assemblyPath: Path.Combine(TempRoot.Root, "Test.dll"), generatedFilesOutputDirectory: null)) .AddAnalyzerReference(analyzerReference); var tempDoc = project.AddDocument("X.cs", SourceText.From("// ")); tempDocId = tempDoc.Id; Assert.True(localWorkspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } using var client = await InProcRemoteHostClient.GetTestClientAsync(localWorkspace); var remoteWorkspace = client.GetRemoteWorkspace(); for (var i = 0; i < values.Length; i++) { sourceTexts = values[i]; // make a change to the project to force a change between the local and oop solutions. Assert.True(localWorkspace.SetCurrentSolution(s => s.WithDocumentText(tempDocId, SourceText.From("// " + i)), WorkspaceChangeKind.SolutionChanged)); await UpdatePrimaryWorkspace(client, localWorkspace.CurrentSolution); var localProject = localWorkspace.CurrentSolution.Projects.Single(); var remoteProject = remoteWorkspace.CurrentSolution.Projects.Single(); // Run generators locally throwIfCalled = false; var localCompilation = await localProject.GetCompilationAsync(); // Now run them remotely. This must not actually call into the generator since nothing has changed. throwIfCalled = true; var remoteCompilation = await remoteProject.GetCompilationAsync(); await AssertSourceGeneratedDocumentsAreSame(localProject, remoteProject, expectedCount: sourceTexts.Length); } static async Task AssertSourceGeneratedDocumentsAreSame(Project localProject, Project remoteProject, int expectedCount) { // The docs on both sides must be in the exact same order, and with identical contents (including // source-text encoding/hash-algorithm). var localGeneratedDocs = (await localProject.GetSourceGeneratedDocumentsAsync()).ToImmutableArray(); var remoteGeneratedDocs = (await remoteProject.GetSourceGeneratedDocumentsAsync()).ToImmutableArray(); Assert.Equal(localGeneratedDocs.Length, remoteGeneratedDocs.Length); Assert.Equal(expectedCount, localGeneratedDocs.Length); for (var i = 0; i < expectedCount; i++) { var localDoc = localGeneratedDocs[i]; var remoteDoc = remoteGeneratedDocs[i]; Assert.Equal(localDoc.HintName, remoteDoc.HintName); Assert.Equal(localDoc.DocumentState.Id, remoteDoc.DocumentState.Id); var localText = await localDoc.GetTextAsync(); var remoteText = await localDoc.GetTextAsync(); Assert.Equal(localText.ToString(), remoteText.ToString()); Assert.Equal(localText.Encoding, remoteText.Encoding); Assert.Equal(localText.ChecksumAlgorithm, remoteText.ChecksumAlgorithm); } } } private static SourceText CreateText(string content, Encoding encoding = null, SourceHashAlgorithm checksumAlgorithm = SourceHashAlgorithm.Sha1) => SourceText.From(content, encoding ?? Encoding.UTF8, checksumAlgorithm); private static SourceText CreateStreamText(string content, bool useBOM, bool useMemoryStream) { var encoding = new UTF8Encoding(useBOM); var bytes = encoding.GetBytes(content); if (useMemoryStream) { using var stream = new MemoryStream(bytes); return SourceText.From(stream, encoding, SourceHashAlgorithm.Sha1, throwIfBinaryDetected: true); } else { return SourceText.From(bytes, bytes.Length, encoding, SourceHashAlgorithm.Sha1, throwIfBinaryDetected: true); } } [Fact] public Task InProcAndRemoteWorkspaceAgree1() => TestInProcAndRemoteWorkspace( ImmutableArray.Create(("SG.cs", CreateText(Guid.NewGuid().ToString())))); [Fact] public async Task InProcAndRemoteWorkspaceAgree2() { var sourceText = CreateText(Guid.NewGuid().ToString()); await TestInProcAndRemoteWorkspace( [("SG.cs", sourceText)], [("SG.cs", sourceText)]); } [Fact] public async Task InProcAndRemoteWorkspaceAgree3() { var sourceText = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(sourceText))], [("SG.cs", CreateText(sourceText))]); } [Fact] public Task InProcAndRemoteWorkspaceAgree4() => TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(Guid.NewGuid().ToString()))], [("SG.cs", CreateText(Guid.NewGuid().ToString()))]); [Fact] public Task InProcAndRemoteWorkspaceAgree5() => TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(Guid.NewGuid().ToString()))], [("NewName.cs", CreateText(Guid.NewGuid().ToString()))]); [Fact] public async Task InProcAndRemoteWorkspaceAgree6() { var sourceText = CreateText(Guid.NewGuid().ToString()); await TestInProcAndRemoteWorkspace( [("SG.cs", sourceText)], [("NewName.cs", sourceText)]); } [Fact] public async Task InProcAndRemoteWorkspaceAgree7() { var sourceText = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(sourceText))], [("NewName.cs", CreateText(sourceText))]); } [Fact] public Task InProcAndRemoteWorkspaceAgree8() => TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(Guid.NewGuid().ToString()))], [("NewName.cs", CreateText(Guid.NewGuid().ToString()))]); [Fact] public Task InProcAndRemoteWorkspaceAgree9() => TestInProcAndRemoteWorkspace( [("SG.cs", CreateText("X", Encoding.ASCII))], [("SG.cs", CreateText("X", Encoding.UTF8))]); [Fact] public Task InProcAndRemoteWorkspaceAgree10() => TestInProcAndRemoteWorkspace( [("SG.cs", CreateText("X", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha1))], [("SG.cs", CreateText("X", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256))]); [Fact] public Task InProcAndRemoteWorkspaceAgree11() => TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(Guid.NewGuid().ToString()))], []); [Fact] public Task InProcAndRemoteWorkspaceAgree12() => TestInProcAndRemoteWorkspace( [], [("SG.cs", CreateText(Guid.NewGuid().ToString()))]); [Fact] public async Task InProcAndRemoteWorkspaceAgree13() { var contents = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(contents))], [("SG.cs", CreateText(contents)), ("SG1.cs", CreateText(contents))]); } [Fact] public async Task InProcAndRemoteWorkspaceAgree14() { var contents = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(contents))], [("SG.cs", CreateText(contents)), ("SG1.cs", CreateText("Other"))]); } [Fact] public async Task InProcAndRemoteWorkspaceAgree15() { var contents = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(contents))], [("SG1.cs", CreateText(contents)), ("SG.cs", CreateText("Other"))]); } [Fact] public async Task InProcAndRemoteWorkspaceAgree16() { var contents = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(contents))], [("SG1.cs", CreateText("Other")), ("SG.cs", CreateText(contents))]); } [Fact] public async Task InProcAndRemoteWorkspaceAgree17() { var contents = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG.cs", CreateText(contents))], [("SG1.cs", CreateText("Other")), ("SG.cs", CreateText(contents))], []); } [Fact] public async Task InProcAndRemoteWorkspaceAgree18() { var contents = CreateText(Guid.NewGuid().ToString()); await TestInProcAndRemoteWorkspace( ImmutableArray.Create(("SG1.cs", contents), ("SG2.cs", contents))); } [Fact] public async Task InProcAndRemoteWorkspaceAgree19() { var contents = CreateText(Guid.NewGuid().ToString()); await TestInProcAndRemoteWorkspace( [("SG1.cs", contents), ("SG2.cs", contents)], [("SG2.cs", contents), ("SG1.cs", contents)]); } [Fact] public async Task InProcAndRemoteWorkspaceAgree20() { var contents = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG1.cs", CreateText(contents)), ("SG2.cs", CreateText(contents))], [("SG2.cs", CreateText(contents)), ("SG1.cs", CreateText(contents))]); } [Theory, CombinatorialData] public async Task InProcAndRemoteWorkspaceAgree21( bool useBOM1, bool useMemoryStream1, bool useBOM2, bool useMemoryStream2) { var contents = Guid.NewGuid().ToString(); await TestInProcAndRemoteWorkspace( [("SG.cs", CreateStreamText(contents, useBOM: useBOM1, useMemoryStream: useMemoryStream1))], [("SG.cs", CreateStreamText(contents, useBOM: useBOM2, useMemoryStream: useMemoryStream2))]); } [PartNotDiscoverable] [ExportWorkspaceService(typeof(IWorkspaceConfigurationService), ServiceLayer.Test), System.Composition.Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] private sealed class TestWorkspaceConfigurationService(IGlobalOptionService globalOptionService) : IWorkspaceConfigurationService { public WorkspaceConfigurationOptions Options => globalOptionService.GetWorkspaceConfigurationOptions(); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_RegenerateOnEdit( SourceGeneratorExecutionPreference executionPreference) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, executionPreference); var callCount = 0; var generator = new CallbackGenerator( onInit: _ => { }, onExecute: _ => { }, computeSourceTexts: () => { callCount++; return ImmutableArray.Create(("hint", SourceText.From($"// generated document {callCount}", Encoding.UTF8))); }); var tempDocId = AddSimpleDocument(workspace, generator); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>(); _ = await client.TryInvokeAsync<IRemoteInitializationService, (int, string)>( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); var project = solution.Projects.Single(); var compilation = await project.GetCompilationAsync(); // Should call the generator the first time. Assert.Equal(1, callCount); solution = solution.WithTextDocumentText(tempDocId, SourceText.From("// new contents")); Assert.True(workspace.SetCurrentSolution(_ => solution, WorkspaceChangeKind.SolutionChanged)); solution = workspace.CurrentSolution; project = solution.Projects.Single(); compilation = await project.GetCompilationAsync(); var sourceGeneratedDocuments = await project.GetSourceGeneratedDocumentsAsync(); Assert.Single(sourceGeneratedDocuments); if (executionPreference is SourceGeneratorExecutionPreference.Automatic) { Assert.Equal(2, callCount); Assert.Equal("// generated document 2", sourceGeneratedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString()); } else { Assert.Equal(1, callCount); Assert.Equal("// generated document 1", sourceGeneratedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString()); } } private static DocumentId AddSimpleDocument(TestWorkspace workspace, CallbackGenerator generator) { var projectId = ProjectId.CreateNewId(); var analyzerReference = new TestGeneratorReference(generator); var project = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId) .WithCompilationOutputInfo(new CompilationOutputInfo( assemblyPath: Path.Combine(TempRoot.Root, "Test.dll"), generatedFilesOutputDirectory: null)) .AddAnalyzerReference(analyzerReference); var tempDoc = project.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); return tempDoc.Id; } private static async Task WaitForSourceGeneratorsAsync(TestWorkspace workspace) { var operations = workspace.ExportProvider.GetExportedValue<AsynchronousOperationListenerProvider>(); await operations.WaitAllAsync(workspace, [FeatureAttribute.Workspace, FeatureAttribute.SourceGenerators]); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_MinorVersionChange_NoActualChange( SourceGeneratorExecutionPreference executionPreference) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, executionPreference); var callCount = 0; var generator = new CallbackGenerator( onInit: _ => { }, onExecute: _ => { }, computeSourceTexts: () => { callCount++; return ImmutableArray.Create(("hint", SourceText.From($"// generated document {callCount}", Encoding.UTF8))); }); var tempDocId = AddSimpleDocument(workspace, generator); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>(); _ = await client.TryInvokeAsync<IRemoteInitializationService, (int, string)>( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); var project = solution.Projects.Single(); var compilation = await project.GetCompilationAsync(); // Should call the generator the first time. Assert.Equal(1, callCount); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); solution = workspace.CurrentSolution; project = solution.Projects.Single(); compilation = await project.GetCompilationAsync(); var sourceGeneratedDocuments = await project.GetSourceGeneratedDocumentsAsync(); Assert.Single(sourceGeneratedDocuments); // In both cases, we only expect to be called once. That's because there was no actual change to // anything else in the compilation. So the generator driver should return only cached data. Assert.Equal(1, callCount); Assert.Equal("// generated document 1", sourceGeneratedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString()); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_MajorVersionChange_NoActualChange( SourceGeneratorExecutionPreference executionPreference) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, executionPreference); var callCount = 0; var generator = new CallbackGenerator( onInit: _ => { }, onExecute: _ => { }, computeSourceTexts: () => { callCount++; return ImmutableArray.Create(("hint", SourceText.From($"// generated document {callCount}", Encoding.UTF8))); }); var tempDocId = AddSimpleDocument(workspace, generator); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>(); _ = await client.TryInvokeAsync<IRemoteInitializationService, (int, string)>( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); var project = solution.Projects.Single(); var compilation = await project.GetCompilationAsync(); // Should call the generator the first time. Assert.Equal(1, callCount); // Because we're forcing regeneration, in both mode we should now see two calls to the generator. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: true); await WaitForSourceGeneratorsAsync(workspace); solution = workspace.CurrentSolution; project = solution.Projects.Single(); compilation = await project.GetCompilationAsync(); var sourceGeneratedDocuments = await project.GetSourceGeneratedDocumentsAsync(); Assert.Single(sourceGeneratedDocuments); Assert.Equal(2, callCount); Assert.Equal("// generated document 2", sourceGeneratedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString()); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_FullSolutionChange_Minor(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; if (sourceGeneratorExecution is SourceGeneratorExecutionPreference.Automatic) { // In automatic mode, nothing should change. Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } else { // In balanced mode, both projects should have their minor version updated. Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMinorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMinorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_FullSolutionChange_Major(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: true); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_FullSolutionChange_MajorAndMinor(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; // forceRegeneration=true should take precedence. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: false); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: true); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_ProjectChange_Minor_1(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; // Updating project1 should only impact it. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId1, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; if (sourceGeneratorExecution is SourceGeneratorExecutionPreference.Automatic) { // In automatic mode, nothing should change. Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); } else { // In balanced mode, only this project should have its minor version changed. Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMinorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); } Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_ProjectChange_Minor_2(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2) .AddProjectReference(new(projectId1)); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; // Updating project1 should regen both projects due to p2p reference. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId1, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; if (sourceGeneratorExecution is SourceGeneratorExecutionPreference.Automatic) { // In automatic mode, nothing should change. Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } else { // In balanced mode, both projects should update their minor version Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMinorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMinorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_ProjectChange_Minor_3(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2) .AddProjectReference(new(projectId1)); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; // Updating project2 should regen only it due to project1 having no reference to it. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId2, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); if (sourceGeneratorExecution is SourceGeneratorExecutionPreference.Automatic) { // In automatic mode, nothing should change. Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } else { // In balanced mode, only the requested project should change. Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMinorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_ProjectChange_MinorAndMajor1(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2) .AddProjectReference(new(projectId1)); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; // Updating project1 should regen both projects due to p2p reference. Force-regen should take precedence // for both as project2 has a ref on project1. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId1, forceRegeneration: true); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId2, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_ProjectChange_MinorAndMajor2(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2) .AddProjectReference(new(projectId1)); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; // Updating project1 should regen both projects due to p2p reference. Force-regen should take precedence // only for project2 as project1 doesn't have a ref on it. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId1, forceRegeneration: false); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId2, forceRegeneration: true); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMinorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_SolutionAndProjectChange_1(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; // Project2 should have a minor update since we only have a solution-minor change. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: false); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId1, forceRegeneration: true); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMinorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_SolutionAndProjectChange_2(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var projectId2 = ProjectId.CreateNewId(); { var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } { var project2 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId2); var tempDoc = project2.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); } var initialSolution = workspace.CurrentSolution; // Project1 and 2 should have a major update since we have a solution-majorchange. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: true); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: projectId1, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); var currentSolution = workspace.CurrentSolution; Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId1).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId1)); Assert.Equal(initialSolution.GetSourceGeneratorExecutionVersion(projectId2).IncrementMajorVersion(), currentSolution.GetSourceGeneratorExecutionVersion(projectId2)); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_NoChange_ButExternalUpdateSignal( SourceGeneratorExecutionPreference executionPreference, bool forceRegeneration) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, executionPreference); var callCount = 0; AddSimpleDocument(workspace, new CallbackGenerator(() => ("hintName.cs", "// callCount: " + callCount++))); var project = workspace.CurrentSolution.Projects.Single(); var documents = await project.GetSourceGeneratedDocumentsAsync(); var document = Assert.Single(documents); Assert.Equal("// callCount: 0", (await document.GetTextAsync()).ToString()); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); document = Assert.Single(documents); if (forceRegeneration) { // In balanced/automatic mode, we were asked to force regenerate. So that should be respected. Assert.Equal("// callCount: 1", (await document.GetTextAsync()).ToString()); } else { // In balanced or automatic mode, since nothing happened and we were not forced, we should not regenerate. Assert.Equal("// callCount: 0", (await document.GetTextAsync()).ToString()); } } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_DocumentChange_ButExternalUpdateSignal( SourceGeneratorExecutionPreference executionPreference, bool forceRegeneration, bool enqueueChangeBeforeEdit, bool enqueueChangeAfterEdit) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, executionPreference); var callCount = 0; var normalDocId = AddSimpleDocument(workspace, new CallbackGenerator(() => ("hintName.cs", "// callCount: " + callCount++))); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>(); // synchronize the remote workspace with the inproc one _ = await client.TryInvokeAsync<IRemoteInitializationService, (int, string)>( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); await WaitForSourceGeneratorsAsync(workspace); var project = workspace.CurrentSolution.Projects.Single(); var documents = await project.GetSourceGeneratedDocumentsAsync(); var document = Assert.Single(documents); Assert.Equal("// callCount: 0", (await document.GetTextAsync()).ToString()); var expectedCallCount = 0; if (enqueueChangeBeforeEdit) { // if we force regeneration we expect generators to run expectedCallCount += forceRegeneration ? 1 : 0; workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration); } await UpdatePrimaryWorkspace(client, solution); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); // Now, make a simple edit to the main document. Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithDocumentText(normalDocId, SourceText.From("// new text")))); // If we're in automatic, we'll always run, because we made a change. expectedCallCount += executionPreference == SourceGeneratorExecutionPreference.Automatic ? 1 : 0; solution = workspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); if (enqueueChangeAfterEdit) { // If we force regeneration we expect to run. // If we are in balanced mode then we expect to run. // In auto we don't expect to run, because we already did for the edit. expectedCallCount += forceRegeneration || executionPreference == SourceGeneratorExecutionPreference.Balanced ? 1 : 0; workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration); } await UpdatePrimaryWorkspace(client, solution); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); document = Assert.Single(documents); Assert.Equal("// callCount: " + expectedCallCount, (await document.GetTextAsync()).ToString()); } [Theory, CombinatorialData] internal async Task TestSourceGenerationExecution_RazorGeneratorAlwaysRuns_OtherGeneratorsRespectPreference(SourceGeneratorExecutionPreference executionPreference) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, executionPreference); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>(); _ = await client.TryInvokeAsync<IRemoteInitializationService, (int, string)>( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var callBackCallCount = 0; var generator1 = new CallbackGenerator(() => ("hintName.cs", "// callCount: " + ++callBackCallCount)); var razorCallCount = 0; var generator2 = new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator((c) => c.AddSource("file.cs", "// callCount: " + ++razorCallCount)); var projectId = ProjectId.CreateNewId(); var project = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId) .WithCompilationOutputInfo(new CompilationOutputInfo( assemblyPath: Path.Combine(TempRoot.Root, "Test.dll"), generatedFilesOutputDirectory: null)) .AddAnalyzerReference(new TestGeneratorReference(generator1)) .AddAnalyzerReference(new TestGeneratorReference(generator2)); var tempDoc = project.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); // Initial: all generators run await ValidateSourceGeneratorDocuments( expectedCallback: 1, expectedRazor: 1); // Now, make a simple edit to the main document. Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithDocumentText(tempDoc.Id, SourceText.From("// new text")))); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); await ValidateSourceGeneratorDocuments( expectedCallback: executionPreference == SourceGeneratorExecutionPreference.Automatic ? 2 : 1, expectedRazor: 2); // Get the documents again and ensure nothing ran await ValidateSourceGeneratorDocuments( expectedCallback: executionPreference == SourceGeneratorExecutionPreference.Automatic ? 2 : 1, expectedRazor: 2); // Make another change, but this time enqueue an update too Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithDocumentText(tempDoc.Id, SourceText.From("// more new text")))); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: false); await ValidateSourceGeneratorDocuments( expectedCallback: executionPreference == SourceGeneratorExecutionPreference.Automatic ? 3 : 2, expectedRazor: 3); async Task ValidateSourceGeneratorDocuments(int expectedCallback, int expectedRazor) { await WaitForSourceGeneratorsAsync(workspace); var project = workspace.CurrentSolution.Projects.Single(); var documents = await project.GetSourceGeneratedDocumentsAsync(); await AssertDocumentTextForGenerator(documents, "Roslyn.Test.Utilities.TestGenerators.CallbackGenerator", expectedCallback); await AssertDocumentTextForGenerator(documents, "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", expectedRazor); } async static Task AssertDocumentTextForGenerator(IEnumerable<SourceGeneratedDocument> docs, string generatorTypeName, int expectedCount) { var text = await docs .Single(d => d.Identity.Generator.TypeName == generatorTypeName) .GetTextAsync(); Assert.Equal($"// callCount: {expectedCount}", text.ToString()); } } [Fact] internal async Task TestSourceGenerationExecution_GeneratorRuns_WhenAddedAfterEarlierRuns_Balanced() { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, SourceGeneratorExecutionPreference.Balanced); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>(); _ = await client.TryInvokeAsync<IRemoteInitializationService, (int, string)>( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var callBackCallCount1 = 0; var generator1 = new CallbackGenerator(() => ("hintName.cs", "// callCount: " + callBackCallCount1++)); var callBackCallCount2 = 0; var generator2 = new CallbackGenerator2((i) => { }, (e) => { e.AddSource("hintName2.cs", "// secondCallCount: " + callBackCallCount2++); }); // add a single generator var projectId = ProjectId.CreateNewId(); var project = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId) .WithCompilationOutputInfo(new CompilationOutputInfo( assemblyPath: Path.Combine(TempRoot.Root, "Test.dll"), generatedFilesOutputDirectory: null)) .AddAnalyzerReference(new TestGeneratorReference(generator1)); var tempDoc = project.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); var documents = await project.GetSourceGeneratedDocumentsAsync(); var doc = Assert.Single(documents); Assert.Equal($"// callCount: 0", (await doc.GetTextAsync()).ToString()); // Now, make a simple edit to the main document. Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithDocumentText(tempDoc.Id, SourceText.From("// new text")))); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); // we don't run because we're not a 'required' generator project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); doc = Assert.Single(documents); Assert.Equal($"// callCount: 0", (await doc.GetTextAsync()).ToString()); // add the second generator Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithProjectAnalyzerReferences(projectId, [.. project.AnalyzerReferences, new TestGeneratorReference(generator2)]))); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); // neither generator will run, because we don't go to OOP at all yet project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); doc = Assert.Single(documents); Assert.Equal($"// callCount: 0", (await doc.GetTextAsync()).ToString()); // make another change, but this time enqueue an update Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithDocumentText(tempDoc.Id, SourceText.From("// more new text")))); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); Assert.Equal(2, documents.Count()); var doc1 = documents.Single(d => d.Name == "hintName.cs"); Assert.Equal($"// callCount: 1", (await doc1.GetTextAsync()).ToString()); var doc2 = documents.Single(d => d.Name == "hintName2.cs"); Assert.Equal($"// secondCallCount: 0", (await doc2.GetTextAsync()).ToString()); } [Fact] internal async Task TestSourceGenerationExecution_RazorGeneratorRuns_WhenAddedAfterEarlierRuns_Balanced() { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, SourceGeneratorExecutionPreference.Balanced); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>(); _ = await client.TryInvokeAsync<IRemoteInitializationService, (int, string)>( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var callBackCallCount = 0; var generator1 = new CallbackGenerator(() => ("hintName.cs", "// callCount: " + callBackCallCount++)); var razorCallCount = 0; var generator2 = new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator((c) => c.AddSource("razor.cs", "// razorCallCount: " + razorCallCount++)); // add a single generator var projectId = ProjectId.CreateNewId(); var project = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId) .WithCompilationOutputInfo(new CompilationOutputInfo( assemblyPath: Path.Combine(TempRoot.Root, "Test.dll"), generatedFilesOutputDirectory: null)) .AddAnalyzerReference(new TestGeneratorReference(generator1)); var tempDoc = project.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); var documents = await project.GetSourceGeneratedDocumentsAsync(); var doc = Assert.Single(documents); Assert.Equal($"// callCount: 0", (await doc.GetTextAsync()).ToString()); // Now, make a simple edit to the main document. Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithDocumentText(tempDoc.Id, SourceText.From("// new text")))); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); // we don't run because we're not a 'required' generator project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); doc = Assert.Single(documents); Assert.Equal($"// callCount: 0", (await doc.GetTextAsync()).ToString()); // add the razor generator Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithProjectAnalyzerReferences(projectId, [.. project.AnalyzerReferences, new TestGeneratorReference(generator2)]))); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); // Razor will run, but the previously added generator will not project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); Assert.Equal(2, documents.Count()); var doc1 = documents.Single(d => d.Name == "hintName.cs"); Assert.Equal($"// callCount: 0", (await doc1.GetTextAsync()).ToString()); var doc2 = documents.Single(d => d.Name == "razor.cs"); Assert.Equal($"// razorCallCount: 0", (await doc2.GetTextAsync()).ToString()); // make another change, but this time enqueue an update Contract.ThrowIfFalse(workspace.TryApplyChanges(workspace.CurrentSolution.WithDocumentText(tempDoc.Id, SourceText.From("// more new text")))); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: false); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); Assert.Equal(2, documents.Count()); doc1 = documents.Single(d => d.Name == "hintName.cs"); Assert.Equal($"// callCount: 1", (await doc1.GetTextAsync()).ToString()); doc2 = documents.Single(d => d.Name == "razor.cs"); Assert.Equal($"// razorCallCount: 1", (await doc2.GetTextAsync()).ToString()); } [Theory, CombinatorialData] [WorkItem("https://github.com/dotnet/roslyn/issues/80714")] internal async Task TestSourceGenerationExecution_AnalyzerReferenceChange_AlwaysObserved( bool queryForSourceGeneratorsPriorToChangeNotification) { using var workspace = CreateWorkspace([typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, SourceGeneratorExecutionPreference.Balanced); using var client = await InProcRemoteHostClient.GetTestClientAsync(workspace).ConfigureAwait(false); var workspaceConfigurationService = workspace.Services.GetRequiredService<IWorkspaceConfigurationService>(); _ = await client.TryInvokeAsync<IRemoteInitializationService, (int, string)>( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var callBackCallCount1 = 0; var generator1 = new CallbackGenerator(() => ("hintName.cs", "// callCount: " + callBackCallCount1++)); var analyzerReference1 = new TestGeneratorReference(generator1); // add a single generator var projectId = ProjectId.CreateNewId(); var project = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId) .WithCompilationOutputInfo(new CompilationOutputInfo( assemblyPath: Path.Combine(TempRoot.Root, "Test.dll"), generatedFilesOutputDirectory: null)) .AddAnalyzerReference(analyzerReference1); var tempDoc = project.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); var documents = await project.GetSourceGeneratedDocumentsAsync(); var doc = Assert.Single(documents); Assert.Equal($"// callCount: 0", (await doc.GetTextAsync()).ToString()); // Now, change the analyzer reference with a new one, simulating that ProjectSystemProjectFactory heard about a // change, and moved the workspace to incorporate it. var analyzerReference2 = new TestGeneratorReference(generator1); workspace.TryApplyChanges(workspace.CurrentSolution.WithProjectAnalyzerReferences(projectId, [analyzerReference1])); await UpdatePrimaryWorkspace(client, workspace.CurrentSolution); await WaitForSourceGeneratorsAsync(workspace); // Simulate a client querying now. We want to make sure that even as we retrieve the same cached SG documents // from the last run, that that won't 'stick' once we hear about the file changes for the analyzer references. if (queryForSourceGeneratorsPriorToChangeNotification) { // We should have the same result as before as nothing will have changed the SG version for this // project so we won't have rerun anything. But we project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); doc = Assert.Single(documents); Assert.Equal($"// callCount: 0", (await doc.GetTextAsync()).ToString()); } // Simulate what we do in ProjectSystemProjectFactory.StartRefreshingAnalyzerReferenceForFileAsync once we have // incorporated the changed analyzer references. Absent this, the above call could cause us to never rerun // until the next build/save (or some other operation that would refresh the SG version). workspace.EnqueueUpdateSourceGeneratorVersion(null, forceRegeneration: true); await WaitForSourceGeneratorsAsync(workspace); project = workspace.CurrentSolution.Projects.Single(); documents = await project.GetSourceGeneratedDocumentsAsync(); doc = Assert.Single(documents); Assert.Equal($"// callCount: 1", (await doc.GetTextAsync()).ToString()); } private static async Task<Solution> VerifyIncrementalUpdatesAsync( TestWorkspace localWorkspace, Workspace remoteWorkspace, RemoteHostClient client, Solution solution, bool applyInBatch, string csAddition, string vbAddition) { var remoteSolution = remoteWorkspace.CurrentSolution; var projectIds = solution.ProjectIds; for (var i = 0; i < projectIds.Count; i++) { var projectName = $"Project{i}"; var project = solution.GetProject(projectIds[i]); var changedDocuments = new List<string>(); var documentIds = project.DocumentIds; for (var j = 0; j < documentIds.Count; j++) { var documentName = $"Document{j}"; var currentSolution = UpdateSolution(solution, projectName, documentName, csAddition, vbAddition); changedDocuments.Add(documentName); solution = currentSolution; if (!applyInBatch) { await UpdateAndVerifyAsync(); } } if (applyInBatch) { await UpdateAndVerifyAsync(); } async Task UpdateAndVerifyAsync() { var documentNames = changedDocuments.ToImmutableArray(); changedDocuments.Clear(); await localWorkspace.ChangeSolutionAsync(solution); solution = localWorkspace.CurrentSolution; await UpdatePrimaryWorkspace(client, solution); var currentRemoteSolution = remoteWorkspace.CurrentSolution; VerifyStates(remoteSolution, currentRemoteSolution, projectName, documentNames); remoteSolution = currentRemoteSolution; Assert.Equal( await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); } } return solution; } [Theory, CombinatorialData] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2085357")] internal async Task TestNonCompilationLanguage(SourceGeneratorExecutionPreference sourceGeneratorExecution) { using var workspace = CreateWorkspace([typeof(NoCompilationLanguageService), typeof(TestWorkspaceConfigurationService)]); var globalOptionService = workspace.ExportProvider.GetExportedValue<IGlobalOptionService>(); globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, sourceGeneratorExecution); // want to access the true workspace solution (which will be a fork of the solution we're producing here). var projectId1 = ProjectId.CreateNewId(); var project1 = workspace.CurrentSolution .AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, name: "Test", assemblyName: "Test", language: LanguageNames.CSharp)) .GetRequiredProject(projectId1); var tempDoc = project1.AddDocument("X.cs", SourceText.From("// ")); Assert.True(workspace.SetCurrentSolution(_ => tempDoc.Project.Solution, WorkspaceChangeKind.SolutionChanged)); var noCompilationProject = workspace.CurrentSolution.AddProject("unknown", "unknown", NoCompilationConstants.LanguageName); Assert.True(workspace.SetCurrentSolution(_ => noCompilationProject.Solution, WorkspaceChangeKind.SolutionChanged)); var initialSolution = workspace.CurrentSolution; var initialExecutionMap = initialSolution.CompilationState.SourceGeneratorExecutionVersionMap.Map; Assert.True(initialExecutionMap.ContainsKey(projectId1)); Assert.True(initialExecutionMap.ContainsKey(noCompilationProject.Id)); // forceRegeneration=true should take precedence. workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: true); await WaitForSourceGeneratorsAsync(workspace); var finalSolution = workspace.CurrentSolution; var finalExecutionMap = finalSolution.CompilationState.SourceGeneratorExecutionVersionMap.Map; Assert.True(finalExecutionMap.ContainsKey(projectId1)); Assert.True(finalExecutionMap.ContainsKey(noCompilationProject.Id)); // We should have successfully changed the version for the C# project. Assert.NotEqual(initialExecutionMap[projectId1], finalExecutionMap[projectId1]); Assert.NotEqual(initialExecutionMap[noCompilationProject.Id], finalExecutionMap[noCompilationProject.Id]); } [Fact] internal async Task TestCopilotChangeAnalysis() { using var workspace = new TestWorkspace(composition: FeaturesTestCompositions.Features); var code = """ class C { void M() { X } } """; workspace.InitializeDocuments(LanguageNames.CSharp, files: [code]); var analyzerReference = new TestAnalyzerReferenceByLanguage(DiagnosticExtensions.GetCompilerDiagnosticAnalyzersMap()); workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences([analyzerReference])); var service = workspace.Services.GetRequiredService<ICopilotChangeAnalysisService>(); var document = workspace.CurrentSolution.Projects.Single().Documents.Single(); var text = await document.GetTextAsync(); var listIndex = text.ToString().IndexOf("X"); var result = await service.AnalyzeChangeAsync( document, [new TextChange(new TextSpan(listIndex, 1), """ <<<<<<< goo Goo ======= bar Bar >>>>>>> baz """)], CancellationToken.None); Assert.True(result.Succeeded); var diagnosticAnalysis = result.DiagnosticAnalyses.Single(d => d.Kind == DiagnosticKind.CompilerSyntax); Assert.Equal(3, diagnosticAnalysis.IdToCount["CS8300"]); Assert.Equal(1, result.CodeFixAnalysis.DiagnosticIdToCount["CS8300"]); Assert.Equal("CSharp.ConflictMarkerResolution.CSharpResolveConflictMarkerCodeFixProvider", result.CodeFixAnalysis.DiagnosticIdToProviderName["CS8300"].Single()); var logger = new TestTelemetryLogger(); Logger.SetLogger(logger); TestTelemetryLogger.TestScope scope; using (CopilotChangeAnalysisUtilities.LogCopilotChangeAnalysis("TestCode", accepted: true, "TestProposalId", result, CancellationToken.None)) { scope = logger.OpenedScopes.Single(); } Logger.SetLogger(null); var endEvent = scope.EndEvent; Assert.Equal("vs/ide/vbcs/copilot/analyzechange", endEvent.Name); var properties = endEvent.Properties; Assert.Equal(true, properties["vs.ide.vbcs.copilot.analyzechange.succeeded"]); Assert.Equal(true, properties["vs.ide.vbcs.copilot.analyzechange.accepted"]); Assert.Equal("TestProposalId", properties["vs.ide.vbcs.copilot.analyzechange.proposalid"]); Assert.Equal(44, properties["vs.ide.vbcs.copilot.analyzechange.olddocumentlength"]); Assert.Equal(90, properties["vs.ide.vbcs.copilot.analyzechange.newdocumentlength"]); Assert.Equal(46, properties["vs.ide.vbcs.copilot.analyzechange.textchangedelta"]); Assert.Equal(1, properties["vs.ide.vbcs.copilot.analyzechange.projectdocumentcount"]); Assert.Equal(0, properties["vs.ide.vbcs.copilot.analyzechange.projectsourcegenerateddocumentcount"]); Assert.Equal(1, properties["vs.ide.vbcs.copilot.analyzechange.projectconecount"]); // No analyzer diagnostics in this scenario. Assert.Equal("", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_analyzersyntax_idtocount"]); Assert.Equal("", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_analyzersyntax_categorytocount"]); Assert.Equal("", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_analyzersyntax_severitytocount"]); Assert.Equal("", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_analyzersemantic_idtocount"]); Assert.Equal("", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_analyzersemantic_categorytocount"]); Assert.Equal("", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_analyzersemantic_severitytocount"]); // CS1002_1 means we got one CS1002 diagnostic. Whereas CS1525_3 means we got 3 CS1525 diagnostics. Assert.Equal("CS1002_1,CS8300_3", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_compilersyntax_idtocount"]); Assert.Equal("Compiler_4", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_compilersyntax_categorytocount"]); Assert.Equal("Error_4", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_compilersyntax_severitytocount"]); Assert.Equal("CS0103_1", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_compilersemantic_idtocount"]); Assert.Equal("Compiler_1", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_compilersemantic_categorytocount"]); Assert.Equal("Error_1", properties["vs.ide.vbcs.copilot.analyzechange.diagnosticanalysis_compilersemantic_severitytocount"]); Assert.Equal("CS0103_2,CS8300_1", properties["vs.ide.vbcs.copilot.analyzechange.codefixanalysis_diagnosticidtocount"]); Assert.Equal("CS0103_CSharp.GenerateVariable.CSharpGenerateVariableCodeFixProvider:CSharp.SpellCheck.CSharpSpellCheckCodeFixProvider,CS8300_CSharp.ConflictMarkerResolution.CSharpResolveConflictMarkerCodeFixProvider", properties["vs.ide.vbcs.copilot.analyzechange.codefixanalysis_diagnosticidtoprovidername"]); } private static void VerifyStates(Solution solution1, Solution solution2, string projectName, ImmutableArray<string> documentNames) { Assert.Equal(WorkspaceKind.RemoteWorkspace, solution1.WorkspaceKind); Assert.Equal(WorkspaceKind.RemoteWorkspace, solution2.WorkspaceKind); SetEqual(solution1.ProjectIds, solution2.ProjectIds); var (project, documents) = GetProjectAndDocuments(solution1, projectName, documentNames); var projectId = project.Id; var documentIds = documents.SelectAsArray(document => document.Id); var projectIds = solution1.ProjectIds; for (var i = 0; i < projectIds.Count; i++) { var currentProjectId = projectIds[i]; var projectStateShouldSame = projectId != currentProjectId; Assert.Equal(projectStateShouldSame, object.ReferenceEquals(solution1.GetProject(currentProjectId).State, solution2.GetProject(currentProjectId).State)); if (!projectStateShouldSame) { SetEqual(solution1.GetProject(currentProjectId).DocumentIds, solution2.GetProject(currentProjectId).DocumentIds); var documentIdsInProject = solution1.GetProject(currentProjectId).DocumentIds; for (var j = 0; j < documentIdsInProject.Count; j++) { var currentDocumentId = documentIdsInProject[j]; var documentStateShouldSame = !documentIds.Contains(currentDocumentId); Assert.Equal(documentStateShouldSame, object.ReferenceEquals(solution1.GetDocument(currentDocumentId).State, solution2.GetDocument(currentDocumentId).State)); } } } } private static async Task VerifyAssetStorageAsync(InProcRemoteHostClient client, Solution solution) { var map = await solution.GetAssetMapAsync(projectConeId: null, CancellationToken.None); var storage = client.TestData.WorkspaceManager.SolutionAssetCache; TestUtils.VerifyAssetStorage(map, storage); } private static Solution UpdateSolution(Solution solution, string projectName, string documentName, string csAddition, string vbAddition) { var (_, document) = GetProjectAndDocument(solution, projectName, documentName); return document.WithText(GetNewText(document, csAddition, vbAddition)).Project.Solution; } private static SourceText GetNewText(Document document, string csAddition, string vbAddition) { if (document.Project.Language == LanguageNames.CSharp) { return SourceText.From(document.State.GetTextSynchronously(CancellationToken.None).ToString() + csAddition); } return SourceText.From(document.State.GetTextSynchronously(CancellationToken.None).ToString() + vbAddition); } private static (Project project, Document document) GetProjectAndDocument(Solution solution, string projectName, string documentName) { var project = solution.Projects.First(p => string.Equals(p.Name, projectName, StringComparison.OrdinalIgnoreCase)); var document = project.Documents.First(d => string.Equals(d.Name, documentName, StringComparison.OrdinalIgnoreCase)); return (project, document); } private static (Project project, ImmutableArray<Document> documents) GetProjectAndDocuments(Solution solution, string projectName, ImmutableArray<string> documentNames) { var project = solution.Projects.First(p => string.Equals(p.Name, projectName, StringComparison.OrdinalIgnoreCase)); var documents = documentNames.SelectAsArray( documentName => project.Documents.First(d => string.Equals(d.Name, documentName, StringComparison.OrdinalIgnoreCase))); return (project, documents); } private static async Task UpdatePrimaryWorkspace(RemoteHostClient client, Solution solution) => await client.TryInvokeAsync<IRemoteAssetSynchronizationService>( solution, async (service, solutionInfo, cancellationToken) => await service.SynchronizePrimaryWorkspaceAsync(solutionInfo, cancellationToken), CancellationToken.None); private static Solution Populate(Solution solution) { solution = AddProject(solution, LanguageNames.CSharp, [ "class CS { }", "class CS2 { }" ], [ "cs additional file content" ], []); solution = AddProject(solution, LanguageNames.VisualBasic, [ "Class VB\r\nEnd Class", "Class VB2\r\nEnd Class" ], [ "vb additional file content" ], [solution.ProjectIds[0]]); solution = AddProject(solution, LanguageNames.CSharp, [ "class Top { }" ], [ "cs additional file content" ], [.. solution.ProjectIds]); solution = AddProject(solution, LanguageNames.CSharp, [ "class OrphanCS { }", "class OrphanCS2 { }" ], [ "cs additional file content", "cs additional file content2" ], []); solution = AddProject(solution, LanguageNames.CSharp, [ "class CS { }", "class CS2 { }", "class CS3 { }", "class CS4 { }", "class CS5 { }", ], [ "cs additional file content" ], []); solution = AddProject(solution, LanguageNames.VisualBasic, [ "Class VB\r\nEnd Class", "Class VB2\r\nEnd Class", "Class VB3\r\nEnd Class", "Class VB4\r\nEnd Class", "Class VB5\r\nEnd Class", ], [ "vb additional file content" ], []); return solution; } private static Solution AddProject( Solution solution, string language, string[] documents, string[] additionalDocuments = null, ProjectId[] p2pReferences = null) { additionalDocuments ??= []; p2pReferences ??= []; var projectName = $"Project{solution.ProjectIds.Count}"; var project = solution.AddProject(projectName, $"{projectName}.dll", language) .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) .AddAnalyzerReference(new AnalyzerFileReference(typeof(object).Assembly.Location, new TestAnalyzerAssemblyLoader())); var projectId = project.Id; solution = project.Solution; for (var i = 0; i < documents.Length; i++) { var current = solution.GetProject(projectId); solution = current.AddDocument($"Document{i}", SourceText.From(documents[i])).Project.Solution; } for (var i = 0; i < additionalDocuments.Length; i++) { var current = solution.GetProject(projectId); solution = current.AddAdditionalDocument($"AdditionalDocument{i}", SourceText.From(additionalDocuments[i])).Project.Solution; } for (var i = 0; i < p2pReferences.Length; i++) { var current = solution.GetProject(projectId); solution = current.AddProjectReference(new ProjectReference(p2pReferences[i])).Solution; } return solution; } private static void SetEqual<T>(IEnumerable<T> expected, IEnumerable<T> actual) { var expectedSet = new HashSet<T>(expected); var result = expected.Count() == actual.Count() && expectedSet.SetEquals(actual); if (!result) { Assert.True(result); } } }