Skip to content

Commit

Permalink
Merge pull request #8607 from davidwengier/RoslynTests
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwengier authored Apr 19, 2023
2 parents f63e2b6 + 7f7752b commit 6c85856
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 16 deletions.
11 changes: 11 additions & 0 deletions Razor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.ProjectEngineHost", "src\Razor\src\Microsoft.AspNetCore.Razor.ProjectEngineHost\Microsoft.AspNetCore.Razor.ProjectEngineHost.csproj", "{2FB4801C-A083-4F08-A4FB-C4910985DE31}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.Test", "src\Razor\test\Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.Test\Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.Test.csproj", "{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -736,6 +738,14 @@ Global
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.Release|Any CPU.Build.0 = Release|Any CPU
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{2FB4801C-A083-4F08-A4FB-C4910985DE31}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}.Release|Any CPU.Build.0 = Release|Any CPU
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -817,6 +827,7 @@ Global
{3E2B6DF5-524F-4909-8A66-7F8C6383620A} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
{2223B8FD-D98A-47BE-94A9-6A3A6B8557B8} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
{2FB4801C-A083-4F08-A4FB-C4910985DE31} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
{70C6EAF1-202B-481B-ADD4-D30DF1396BDE} = {92463391-81BE-462B-AC3C-78C6C760741F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.Dispose() -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.EnsureInitialized(Microsoft.CodeAnalysis.Workspace! workspace, string! projectRazorJsonFileName) -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.RazorWorkspaceListener() -> void
Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.RazorWorkspaceListener() -> void
virtual Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.RazorWorkspaceListener.SerializeProjectAsync(Microsoft.CodeAnalysis.Project! project, System.Threading.CancellationToken ct) -> System.Threading.Tasks.Task!
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace;
Expand All @@ -11,15 +10,12 @@ public class RazorWorkspaceListener : IDisposable
private static readonly TimeSpan s_debounceTime = TimeSpan.FromMilliseconds(100);

private string? _projectRazorJsonFileName;
private readonly Dictionary<string, TaskDelayScheduler> _workQueues;
private readonly Dictionary<ProjectId, TaskDelayScheduler> _workQueues;
private readonly object _gate = new();

public RazorWorkspaceListener()
{
var comparer = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? StringComparer.Ordinal
: StringComparer.OrdinalIgnoreCase;
_workQueues = new Dictionary<string, TaskDelayScheduler>(comparer);
_workQueues = new Dictionary<ProjectId, TaskDelayScheduler>();
}

public void EnsureInitialized(Workspace workspace, string projectRazorJsonFileName)
Expand All @@ -38,21 +34,39 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
{
switch (e.Kind)
{
case WorkspaceChangeKind.SolutionAdded:
case WorkspaceChangeKind.SolutionChanged:
case WorkspaceChangeKind.SolutionReloaded:
foreach (var project in e.OldSolution.Projects)
{
RemoveProject(project);
}

foreach (var project in e.NewSolution.Projects)
{
EnqueueUpdate(project);
}

break;

case WorkspaceChangeKind.SolutionAdded:
foreach (var project in e.NewSolution.Projects)
{
EnqueueUpdate(project);
}

break;

case WorkspaceChangeKind.ProjectRemoved:
RemoveProject(e.OldSolution.GetProject(e.ProjectId));
break;

case WorkspaceChangeKind.ProjectReloaded:
RemoveProject(e.OldSolution.GetProject(e.ProjectId));
EnqueueUpdate(e.NewSolution.GetProject(e.ProjectId));
break;

case WorkspaceChangeKind.ProjectAdded:
case WorkspaceChangeKind.ProjectChanged:
case WorkspaceChangeKind.ProjectReloaded:
case WorkspaceChangeKind.DocumentAdded:
case WorkspaceChangeKind.DocumentRemoved:
case WorkspaceChangeKind.DocumentReloaded:
Expand All @@ -66,7 +80,12 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs
case WorkspaceChangeKind.AnalyzerConfigDocumentRemoved:
case WorkspaceChangeKind.AnalyzerConfigDocumentReloaded:
case WorkspaceChangeKind.AnalyzerConfigDocumentChanged:
EnqueueUpdate(e.NewSolution.GetProject(e.ProjectId));
var projectId = e.ProjectId ?? e.DocumentId?.ProjectId;
if (projectId is not null)
{
EnqueueUpdate(e.NewSolution.GetProject(projectId));
}

break;
case WorkspaceChangeKind.SolutionCleared:
case WorkspaceChangeKind.SolutionRemoved:
Expand All @@ -83,17 +102,17 @@ private void Workspace_WorkspaceChanged(object? sender, WorkspaceChangeEventArgs

private void RemoveProject(Project? project)
{
if (project?.FilePath is null)
if (project is null)
{
return;
}

TaskDelayScheduler? scheduler;
lock (_gate)
{
if (_workQueues.TryGetValue(project.FilePath, out scheduler))
if (_workQueues.TryGetValue(project.Id, out scheduler))
{
_workQueues.Remove(project.FilePath);
_workQueues.Remove(project.Id);
}
}

Expand All @@ -105,7 +124,6 @@ private void EnqueueUpdate(Project? project)
if (_projectRazorJsonFileName is null ||
project is not
{
FilePath: not null,
Language: LanguageNames.CSharp
})
{
Expand All @@ -115,13 +133,25 @@ project is not
TaskDelayScheduler? scheduler;
lock (_gate)
{
if (!_workQueues.TryGetValue(project.FilePath, out scheduler))
if (!_workQueues.TryGetValue(project.Id, out scheduler))
{
scheduler = new TaskDelayScheduler(s_debounceTime, CancellationToken.None);
_workQueues.Add(project.Id, scheduler);
}
}

scheduler.ScheduleAsyncTask(ct => RazorProjectJsonSerializer.SerializeAsync(project, _projectRazorJsonFileName, ct), CancellationToken.None);
scheduler.ScheduleAsyncTask(ct => SerializeProjectAsync(project, ct), CancellationToken.None);
}

// Protected for testing
protected virtual Task SerializeProjectAsync(Project project, CancellationToken ct)
{
if (_projectRazorJsonFileName is null)
{
return Task.CompletedTask;
}

return RazorProjectJsonSerializer.SerializeAsync(project, _projectRazorJsonFileName, ct);
}

public void Dispose()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(DefaultNetCoreTargetFrameworks)</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="..\OSSkipConditionFactAttribute.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace\Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Razor.Test.Common\Microsoft.AspNetCore.Razor.Test.Common.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
using Xunit;

namespace Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.Test;

public class RazorWorkspaceListenerTest
{
[Fact]
public async Task ProjectAdded_SchedulesTask()
{
using var workspace = new AdhocWorkspace(CodeAnalysis.Host.Mef.MefHostServices.DefaultHost);

using var listener = new TestRazorWorkspaceListener();
listener.EnsureInitialized(workspace, "temp.json");

var project = workspace.AddProject("TestProject", LanguageNames.CSharp);

// Wait for debounce
await Task.Delay(500);

Assert.Equal(1, listener.SerializeCalls[project.Id]);
}

[Fact]
public async Task TwoProjectsAdded_SchedulesTwoTasks()
{
using var workspace = new AdhocWorkspace(CodeAnalysis.Host.Mef.MefHostServices.DefaultHost);

using var listener = new TestRazorWorkspaceListener();
listener.EnsureInitialized(workspace, "temp.json");

var project1 = workspace.AddProject("TestProject1", LanguageNames.CSharp);
var project2 = workspace.AddProject("TestProject2", LanguageNames.CSharp);

// Wait for debounce
await Task.Delay(500);

// These are different projects, so should cause two calls
Assert.Equal(1, listener.SerializeCalls[project1.Id]);
Assert.Equal(1, listener.SerializeCalls[project2.Id]);
}

[Fact]
public async Task ProjectAddedAndRemoved_NoTasks()
{
using var workspace = new AdhocWorkspace(CodeAnalysis.Host.Mef.MefHostServices.DefaultHost);

using var listener = new TestRazorWorkspaceListener();
listener.EnsureInitialized(workspace, "temp.json");

var project = workspace.AddProject("TestProject", LanguageNames.CSharp);
var newSolution = project.Solution.RemoveProject(project.Id);
Assert.True(workspace.TryApplyChanges(newSolution));

// Wait for debounce
await Task.Delay(500);

Assert.Empty(listener.SerializeCalls);
}

[Fact]
public async Task TwoProjectChanges_SchedulesOneTasks()
{
using var workspace = new AdhocWorkspace(CodeAnalysis.Host.Mef.MefHostServices.DefaultHost);

using var listener = new TestRazorWorkspaceListener();
listener.EnsureInitialized(workspace, "temp.json");

var project = workspace.AddProject("TestProject", LanguageNames.CSharp);
var newSolution = project.Solution.WithProjectDefaultNamespace(project.Id, "NewDefaultNamespace");
Assert.True(workspace.TryApplyChanges(newSolution));

// Wait for debounce
await Task.Delay(500);

Assert.Equal(1, listener.SerializeCalls[project.Id]);
}

[Fact]
public async Task DocumentAdded_SchedulesTask()
{
using var workspace = new AdhocWorkspace(CodeAnalysis.Host.Mef.MefHostServices.DefaultHost);

using var listener = new TestRazorWorkspaceListener();
listener.EnsureInitialized(workspace, "temp.json");

var project = workspace.AddProject("TestProject", LanguageNames.CSharp);

workspace.AddDocument(project.Id, "Document", SourceText.From("Hi there"));

// Wait for debounce
await Task.Delay(500);

Assert.Equal(1, listener.SerializeCalls[project.Id]);
}

[Fact]
public async Task DocumentAdded_WithDelay_SchedulesTwoTasks()
{
using var workspace = new AdhocWorkspace(CodeAnalysis.Host.Mef.MefHostServices.DefaultHost);

using var listener = new TestRazorWorkspaceListener();
listener.EnsureInitialized(workspace, "temp.json");

var project = workspace.AddProject("TestProject", LanguageNames.CSharp);

// Wait for debounce
await Task.Delay(500);

workspace.AddDocument(project.Id, "Document", SourceText.From("Hi there"));

// Wait for debounce
await Task.Delay(500);

Assert.Equal(2, listener.SerializeCalls[project.Id]);
}

private class TestRazorWorkspaceListener : RazorWorkspaceListener
{
private Dictionary<ProjectId, int> _serializeCalls = new();

public Dictionary<ProjectId, int> SerializeCalls => _serializeCalls;

protected override Task SerializeProjectAsync(Project project, CancellationToken ct)
{
if (!_serializeCalls.ContainsKey(project.Id))
{
_serializeCalls.Add(project.Id, 0);
}

_serializeCalls[project.Id]++;

return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"methodDisplay": "method",
"shadowCopy": false,
"parallelizeTestCollections": false
}

0 comments on commit 6c85856

Please sign in to comment.