|
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license.
|
3 | 3 | // See the LICENSE file in the project root for more information.
|
4 | 4 |
|
| 5 | +using System.Diagnostics.CodeAnalysis; |
5 | 6 | using System.Linq;
|
6 | 7 | using System.Threading;
|
7 | 8 | using System.Threading.Tasks;
|
|
11 | 12 | using Microsoft.CodeAnalysis.Text;
|
12 | 13 | using Roslyn.Test.Utilities;
|
13 | 14 | using Roslyn.Test.Utilities.TestGenerators;
|
14 |
| -using Roslyn.Utilities; |
15 | 15 | using Xunit;
|
16 | 16 | using Xunit.Abstractions;
|
17 | 17 | using LSP = Roslyn.LanguageServer.Protocol;
|
@@ -223,6 +223,103 @@ internal async Task TestReturnsGeneratedSourceWhenManuallyRefreshed(bool mutatin
|
223 | 223 | Assert.Equal("// callCount: 1", secondRequest.Text);
|
224 | 224 | }
|
225 | 225 |
|
| 226 | + [Theory, CombinatorialData] |
| 227 | + internal async Task TestCanRunSourceGeneratorAndApplyChangesConcurrently( |
| 228 | + bool mutatingLspWorkspace, |
| 229 | + bool majorVersionUpdate, |
| 230 | + SourceGeneratorExecutionPreference sourceGeneratorExecution) |
| 231 | + { |
| 232 | + await using var testLspServer = await CreateTestLspServerAsync(""" |
| 233 | + class C |
| 234 | + { |
| 235 | + } |
| 236 | + """, mutatingLspWorkspace); |
| 237 | + |
| 238 | + var configService = testLspServer.TestWorkspace.ExportProvider.GetExportedValue<TestWorkspaceConfigurationService>(); |
| 239 | + configService.Options = new WorkspaceConfigurationOptions(SourceGeneratorExecution: sourceGeneratorExecution); |
| 240 | + |
| 241 | + var callCount = 0; |
| 242 | + var generatorReference = await AddGeneratorAsync(new CallbackGenerator(() => ("hintName.cs", "// callCount: " + callCount++)), testLspServer.TestWorkspace); |
| 243 | + |
| 244 | + var sourceGeneratedDocuments = await testLspServer.GetCurrentSolution().Projects.Single().GetSourceGeneratedDocumentsAsync(); |
| 245 | + var sourceGeneratedDocumentIdentity = sourceGeneratedDocuments.Single().Identity; |
| 246 | + var sourceGeneratorDocumentUri = SourceGeneratedDocumentUri.Create(sourceGeneratedDocumentIdentity); |
| 247 | + |
| 248 | + var text = await testLspServer.ExecuteRequestAsync<SourceGeneratorGetTextParams, SourceGeneratedDocumentText>(SourceGeneratedDocumentGetTextHandler.MethodName, |
| 249 | + new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sourceGeneratorDocumentUri }, ResultId: null), CancellationToken.None); |
| 250 | + |
| 251 | + AssertEx.NotNull(text); |
| 252 | + Assert.Equal("// callCount: 0", text.Text); |
| 253 | + |
| 254 | + var initialSolution = testLspServer.GetCurrentSolution(); |
| 255 | + var initialExecutionMap = initialSolution.CompilationState.SourceGeneratorExecutionVersionMap.Map; |
| 256 | + |
| 257 | + // Updating the execution version should trigger source generators to run in both automatic and balanced mode. |
| 258 | + var forceRegeneration = majorVersionUpdate; |
| 259 | + testLspServer.TestWorkspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration); |
| 260 | + await testLspServer.WaitForSourceGeneratorsAsync(); |
| 261 | + |
| 262 | + var solutionWithChangedExecutionVersion = testLspServer.GetCurrentSolution(); |
| 263 | + |
| 264 | + var secondRequest = await testLspServer.ExecuteRequestAsync<SourceGeneratorGetTextParams, SourceGeneratedDocumentText>(SourceGeneratedDocumentGetTextHandler.MethodName, |
| 265 | + new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sourceGeneratorDocumentUri }, ResultId: text.ResultId), CancellationToken.None); |
| 266 | + AssertEx.NotNull(secondRequest); |
| 267 | + |
| 268 | + if (forceRegeneration) |
| 269 | + { |
| 270 | + Assert.NotEqual(text.ResultId, secondRequest.ResultId); |
| 271 | + Assert.Equal("// callCount: 1", secondRequest.Text); |
| 272 | + } |
| 273 | + else |
| 274 | + { |
| 275 | + Assert.Equal(text.ResultId, secondRequest.ResultId); |
| 276 | + Assert.Null(secondRequest.Text); |
| 277 | + } |
| 278 | + |
| 279 | + var projectId1 = initialSolution.ProjectIds.Single(); |
| 280 | + var solutionWithDocumentChanged = initialSolution.WithDocumentText( |
| 281 | + initialSolution.Projects.Single().Documents.Single().Id, |
| 282 | + SourceText.From("class D { }")); |
| 283 | + |
| 284 | + var expectVersionChange = sourceGeneratorExecution is SourceGeneratorExecutionPreference.Balanced || forceRegeneration; |
| 285 | + |
| 286 | + // The content forked solution should have an SG execution version *less than* the one we just changed. |
| 287 | + // Note: this will be patched up once we call TryApplyChanges. |
| 288 | + if (expectVersionChange) |
| 289 | + { |
| 290 | + Assert.True( |
| 291 | + solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1] |
| 292 | + > solutionWithDocumentChanged.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); |
| 293 | + } |
| 294 | + else |
| 295 | + { |
| 296 | + Assert.Equal( |
| 297 | + solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1], |
| 298 | + solutionWithDocumentChanged.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); |
| 299 | + } |
| 300 | + |
| 301 | + Assert.True(testLspServer.TestWorkspace.TryApplyChanges(solutionWithDocumentChanged)); |
| 302 | + |
| 303 | + var finalSolution = testLspServer.GetCurrentSolution(); |
| 304 | + |
| 305 | + if (expectVersionChange) |
| 306 | + { |
| 307 | + // In balanced (or if we forced regen) mode, the execution version should have been updated to the new value. |
| 308 | + Assert.NotEqual(initialExecutionMap[projectId1], solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); |
| 309 | + Assert.NotEqual(initialExecutionMap[projectId1], finalSolution.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); |
| 310 | + } |
| 311 | + else |
| 312 | + { |
| 313 | + // In automatic mode, nothing should change wrt to execution versions (unless we specified force-regenerate). |
| 314 | + Assert.Equal(initialExecutionMap[projectId1], solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); |
| 315 | + Assert.Equal(initialExecutionMap[projectId1], finalSolution.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); |
| 316 | + } |
| 317 | + |
| 318 | + // The final execution version for the project should match the changed execution version, no matter what. |
| 319 | + // Proving that the content change happened, but didn't drop the execution version change. |
| 320 | + Assert.Equal(solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1], finalSolution.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); |
| 321 | + } |
| 322 | + |
226 | 323 | [Theory, CombinatorialData]
|
227 | 324 | public async Task TestReturnsNullForRemovedClosedGeneratedFile(bool mutatingLspWorkspace)
|
228 | 325 | {
|
@@ -279,7 +376,9 @@ public async Task TestReturnsNullForRemovedOpenedGeneratedFile(bool mutatingLspW
|
279 | 376 | Assert.Null(secondRequest.Text);
|
280 | 377 | }
|
281 | 378 |
|
282 |
| - private async Task<TestLspServer> CreateTestLspServerWithGeneratorAsync(bool mutatingLspWorkspace, string generatedDocumentText) |
| 379 | + private async Task<TestLspServer> CreateTestLspServerWithGeneratorAsync( |
| 380 | + bool mutatingLspWorkspace, |
| 381 | + [StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string generatedDocumentText) |
283 | 382 | {
|
284 | 383 | var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace);
|
285 | 384 | await AddGeneratorAsync(new SingleFileTestGenerator(generatedDocumentText), testLspServer.TestWorkspace);
|
|
0 commit comments