This change cleans up the `Microsoft.VisualStudio.LiveShare.Razor`
project and its unit tests. In addition, it marks several types as
public to avoid `TypeLoadExceptions` when Live Share is connected.
Essentially, any services that are proxied by live share from the guest
to the host need to have public contracts; otherwise, Live Share's RPC
infrastructure fails to create the proxy. I introduced this failure
seven months ago with
4ffcd3f78e,
but it seems that the failure was never reported.
This commit is contained in:
Dustin Campbell 2024-02-09 08:42:11 -08:00 коммит произвёл GitHub
Родитель 5ea9b03e46 e28eafdb29
Коммит dec4b332f9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
34 изменённых файлов: 669 добавлений и 773 удалений

Просмотреть файл

@ -1,24 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.ComponentModel.Composition;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
[Export(typeof(LiveShareSessionAccessor))]
internal class DefaultLiveShareSessionAccessor : LiveShareSessionAccessor
{
private CollaborationSession? _currentSession;
private bool _guestSessionIsActive;
// We have a separate IsGuestSessionActive to avoid loading LiveShare dlls unnecessarily.
public override bool IsGuestSessionActive => _guestSessionIsActive;
public override CollaborationSession? Session => _currentSession;
public void SetSession(CollaborationSession? session)
{
_guestSessionIsActive = session is not null;
_currentSession = session;
}
}

Просмотреть файл

@ -1,59 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.ComponentModel.Composition;
using System.Threading;
using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
[Export(typeof(ProxyAccessor))]
internal class DefaultProxyAccessor : ProxyAccessor
{
private readonly LiveShareSessionAccessor _liveShareSessionAccessor;
private readonly JoinableTaskFactory _joinableTaskFactory;
private IProjectHierarchyProxy? _projectHierarchyProxy;
[ImportingConstructor]
public DefaultProxyAccessor(
LiveShareSessionAccessor liveShareSessionAccessor,
JoinableTaskContext joinableTaskContext)
{
if (liveShareSessionAccessor is null)
{
throw new ArgumentNullException(nameof(liveShareSessionAccessor));
}
if (joinableTaskContext is null)
{
throw new ArgumentNullException(nameof(joinableTaskContext));
}
_liveShareSessionAccessor = liveShareSessionAccessor;
_joinableTaskFactory = joinableTaskContext.Factory;
}
// Testing constructor
#pragma warning disable CS8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable.
private protected DefaultProxyAccessor()
#pragma warning restore CS8618 // Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable.
{
}
public override IProjectHierarchyProxy GetProjectHierarchyProxy()
{
_projectHierarchyProxy ??= CreateServiceProxy<IProjectHierarchyProxy>();
return _projectHierarchyProxy;
}
// Internal virtual for testing
internal virtual TProxy CreateServiceProxy<TProxy>() where TProxy : class
{
Assumes.NotNull(_liveShareSessionAccessor.Session);
#pragma warning disable VSTHRD110 // Observe result of async calls
return _joinableTaskFactory.Run(() => _liveShareSessionAccessor.Session.GetRemoteServiceAsync<TProxy>(typeof(TProxy).Name, CancellationToken.None));
#pragma warning restore VSTHRD110 // Observe result of async calls
}
}

Просмотреть файл

@ -17,13 +17,13 @@ namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
internal class GuestProjectPathProvider(
JoinableTaskContext joinableTaskContext,
ITextDocumentFactoryService textDocumentFactory,
ProxyAccessor proxyAccessor,
LiveShareSessionAccessor liveShareSessionAccessor) : ILiveShareProjectPathProvider
IProxyAccessor proxyAccessor,
ILiveShareSessionAccessor liveShareSessionAccessor) : ILiveShareProjectPathProvider
{
private readonly JoinableTaskFactory _joinableTaskFactory = joinableTaskContext.Factory;
private readonly ITextDocumentFactoryService _textDocumentFactory = textDocumentFactory;
private readonly ProxyAccessor _proxyAccessor = proxyAccessor;
private readonly LiveShareSessionAccessor _liveShareSessionAccessor = liveShareSessionAccessor;
private readonly IProxyAccessor _proxyAccessor = proxyAccessor;
private readonly ILiveShareSessionAccessor _liveShareSessionAccessor = liveShareSessionAccessor;
public bool TryGetProjectPath(ITextBuffer textBuffer, [NotNullWhen(returnValue: true)] out string? filePath)
{
@ -51,8 +51,7 @@ internal class GuestProjectPathProvider(
return true;
}
// Internal virtual for testing
internal virtual Uri? GetHostProjectPath(ITextDocument textDocument)
private Uri? GetHostProjectPath(ITextDocument textDocument)
{
Assumes.NotNull(_liveShareSessionAccessor.Session);
@ -75,6 +74,8 @@ internal class GuestProjectPathProvider(
[MethodImpl(MethodImplOptions.NoInlining)]
private string ResolveGuestPath(Uri hostProjectPath)
{
return _liveShareSessionAccessor.Session!.ConvertSharedUriToLocalPath(hostProjectPath);
Assumes.NotNull(_liveShareSessionAccessor.Session);
return _liveShareSessionAccessor.Session.ConvertSharedUriToLocalPath(hostProjectPath);
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
internal interface ILiveShareSessionAccessor
{
CollaborationSession? Session { get; }
bool IsGuestSessionActive { get; }
}

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
internal interface IProxyAccessor
{
IProjectHierarchyProxy GetProjectHierarchyProxy();
}

Просмотреть файл

@ -1,11 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.ComponentModel.Composition;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
internal abstract class LiveShareSessionAccessor
[Export(typeof(ILiveShareSessionAccessor))]
internal class LiveShareSessionAccessor : ILiveShareSessionAccessor
{
public abstract CollaborationSession? Session { get; }
private CollaborationSession? _currentSession;
private bool _guestSessionIsActive;
public abstract bool IsGuestSessionActive { get; }
// We have a separate IsGuestSessionActive to avoid loading LiveShare dlls unnecessarily.
public bool IsGuestSessionActive => _guestSessionIsActive;
public CollaborationSession? Session => _currentSession;
public void SetSession(CollaborationSession? session)
{
_guestSessionIsActive = session is not null;
_currentSession = session;
}
}

Просмотреть файл

@ -13,30 +13,20 @@ using IAsyncDisposable = Microsoft.VisualStudio.Threading.IAsyncDisposable;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
internal class ProjectSnapshotSynchronizationService : ICollaborationService, IAsyncDisposable, System.IAsyncDisposable
internal class ProjectSnapshotSynchronizationService(
CollaborationSession sessionContext,
IProjectSnapshotManagerProxy hostProjectManagerProxy,
IProjectSnapshotManagerAccessor projectManagerAccessor,
ProjectSnapshotManagerDispatcher dispatcher,
IErrorReporter errorReporter,
JoinableTaskFactory jtf) : ICollaborationService, IAsyncDisposable, System.IAsyncDisposable
{
private readonly JoinableTaskFactory _joinableTaskFactory;
private readonly CollaborationSession _sessionContext;
private readonly IProjectSnapshotManagerProxy _hostProjectManagerProxy;
private readonly IProjectSnapshotManagerAccessor _projectManagerAccessor;
private readonly IErrorReporter _errorReporter;
private readonly ProjectSnapshotManagerDispatcher _dispatcher;
public ProjectSnapshotSynchronizationService(
JoinableTaskFactory joinableTaskFactory,
CollaborationSession sessionContext,
IProjectSnapshotManagerProxy hostProjectManagerProxy,
IProjectSnapshotManagerAccessor projectManagerAccessor,
IErrorReporter errorReporter,
ProjectSnapshotManagerDispatcher dispatcher)
{
_joinableTaskFactory = joinableTaskFactory;
_sessionContext = sessionContext;
_hostProjectManagerProxy = hostProjectManagerProxy;
_projectManagerAccessor = projectManagerAccessor;
_errorReporter = errorReporter;
_dispatcher = dispatcher;
}
private readonly JoinableTaskFactory _jtf = jtf;
private readonly CollaborationSession _sessionContext = sessionContext;
private readonly IProjectSnapshotManagerProxy _hostProjectManagerProxy = hostProjectManagerProxy;
private readonly IProjectSnapshotManagerAccessor _projectManagerAccessor = projectManagerAccessor;
private readonly IErrorReporter _errorReporter = errorReporter;
private readonly ProjectSnapshotManagerDispatcher _dispatcher = dispatcher;
public async Task InitializeAsync(CancellationToken cancellationToken)
{
@ -153,7 +143,7 @@ internal class ProjectSnapshotSynchronizationService : ICollaborationService, IA
throw new ArgumentNullException(nameof(args));
}
_joinableTaskFactory.Run(async () =>
_jtf.Run(async () =>
{
try
{

Просмотреть файл

@ -15,10 +15,10 @@ namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
[ExportCollaborationService(typeof(ProjectSnapshotSynchronizationService), Scope = SessionScope.Guest)]
[method: ImportingConstructor]
internal class ProjectSnapshotSynchronizationServiceFactory(
JoinableTaskContext joinableTaskContext,
IProjectSnapshotManagerAccessor projectManagerAccessor,
ProjectSnapshotManagerDispatcher dispatcher,
IErrorReporter errorReporter,
ProjectSnapshotManagerDispatcher dispatcher) : ICollaborationServiceFactory
JoinableTaskContext joinableTaskContext) : ICollaborationServiceFactory
{
public async Task<ICollaborationService> CreateServiceAsync(CollaborationSession sessionContext, CancellationToken cancellationToken)
{
@ -29,12 +29,12 @@ internal class ProjectSnapshotSynchronizationServiceFactory(
var projectSnapshotManagerProxy = await sessionContext.GetRemoteServiceAsync<IProjectSnapshotManagerProxy>(typeof(IProjectSnapshotManagerProxy).Name, cancellationToken);
var synchronizationService = new ProjectSnapshotSynchronizationService(
joinableTaskContext.Factory,
sessionContext,
projectSnapshotManagerProxy,
projectManagerAccessor,
dispatcher,
errorReporter,
dispatcher);
joinableTaskContext.Factory);
await synchronizationService.InitializeAsync(cancellationToken);

Просмотреть файл

@ -1,9 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.ComponentModel.Composition;
using System.Threading;
using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
internal abstract class ProxyAccessor
[Export(typeof(IProxyAccessor))]
[method: ImportingConstructor]
internal class ProxyAccessor(
ILiveShareSessionAccessor liveShareSessionAccessor,
JoinableTaskContext joinableTaskContext) : IProxyAccessor
{
public abstract IProjectHierarchyProxy GetProjectHierarchyProxy();
private readonly ILiveShareSessionAccessor _liveShareSessionAccessor = liveShareSessionAccessor;
private readonly JoinableTaskFactory _jtf = joinableTaskContext.Factory;
private IProjectHierarchyProxy? _projectHierarchyProxy;
public IProjectHierarchyProxy GetProjectHierarchyProxy()
=> _projectHierarchyProxy ??= CreateServiceProxy<IProjectHierarchyProxy>();
private TProxy CreateServiceProxy<TProxy>() where TProxy : class
{
Assumes.NotNull(_liveShareSessionAccessor.Session);
return _jtf.Run(
() => _liveShareSessionAccessor.Session.GetRemoteServiceAsync<TProxy>(typeof(TProxy).Name, CancellationToken.None));
}
}

Просмотреть файл

@ -12,35 +12,19 @@ using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
[ExportCollaborationService(typeof(SessionActiveDetector), Scope = SessionScope.Guest)]
internal class RazorGuestInitializationService : ICollaborationServiceFactory
[method: ImportingConstructor]
internal class RazorGuestInitializationService(
[Import(typeof(ILiveShareSessionAccessor))] LiveShareSessionAccessor sessionAccessor) : ICollaborationServiceFactory
{
private const string ViewImportsFileName = "_ViewImports.cshtml";
private readonly DefaultLiveShareSessionAccessor _sessionAccessor;
private readonly LiveShareSessionAccessor _sessionAccessor = sessionAccessor;
// Internal for testing
internal Task? _viewImportsCopyTask;
[ImportingConstructor]
public RazorGuestInitializationService([Import(typeof(LiveShareSessionAccessor))] DefaultLiveShareSessionAccessor sessionAccessor)
{
if (sessionAccessor is null)
{
throw new ArgumentNullException(nameof(sessionAccessor));
}
_sessionAccessor = sessionAccessor;
}
private Task? _viewImportsCopyTask;
public Task<ICollaborationService> CreateServiceAsync(CollaborationSession sessionContext, CancellationToken cancellationToken)
{
if (sessionContext is null)
{
throw new ArgumentNullException(nameof(sessionContext));
}
#pragma warning disable CA2000 // Dispose objects before losing scope
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
#pragma warning restore CA2000 // Dispose objects before losing scope
_viewImportsCopyTask = EnsureViewImportsCopiedAsync(sessionContext, cts.Token);
_sessionAccessor.SetSession(sessionContext);
@ -99,6 +83,14 @@ internal class RazorGuestInitializationService : ICollaborationServiceFactory
}
}
}
internal TestAccessor GetTestAccessor()
=> new(this);
internal sealed class TestAccessor(RazorGuestInitializationService instance)
{
public Task? ViewImportsCopyTask => instance._viewImportsCopyTask;
}
}
internal class SessionActiveDetector(Action onDispose) : ICollaborationService, IDisposable

Просмотреть файл

@ -1,45 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.LiveShare.Razor.Host;
[ExportCollaborationService(
typeof(IProjectHierarchyProxy),
Name = nameof(IProjectHierarchyProxy),
Scope = SessionScope.Host,
Role = ServiceRole.RemoteService)]
internal class DefaultProjectHierarchyProxyFactory : ICollaborationServiceFactory
{
private readonly JoinableTaskContext _joinableTaskContext;
[ImportingConstructor]
public DefaultProjectHierarchyProxyFactory(
ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher,
JoinableTaskContext joinableTaskContext)
{
if (joinableTaskContext is null)
{
throw new ArgumentNullException(nameof(joinableTaskContext));
}
_joinableTaskContext = joinableTaskContext;
}
public Task<ICollaborationService> CreateServiceAsync(CollaborationSession session, CancellationToken cancellationToken)
{
if (session is null)
{
throw new ArgumentNullException(nameof(session));
}
var service = new DefaultProjectHierarchyProxy(session, _joinableTaskContext.Factory);
return Task.FromResult<ICollaborationService>(service);
}
}

Просмотреть файл

@ -10,31 +10,17 @@ using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.LiveShare.Razor.Host;
internal class DefaultProjectHierarchyProxy : IProjectHierarchyProxy, ICollaborationService
internal class ProjectHierarchyProxy(
CollaborationSession session,
IServiceProvider serviceProvider,
JoinableTaskFactory jtf) : IProjectHierarchyProxy, ICollaborationService
{
private readonly CollaborationSession _session;
private readonly CollaborationSession _session = session;
private readonly IServiceProvider _serviceProvider = serviceProvider;
private readonly JoinableTaskFactory _jtf = jtf;
private readonly JoinableTaskFactory _joinableTaskFactory;
private IVsUIShellOpenDocument? _openDocumentShell;
public DefaultProjectHierarchyProxy(
CollaborationSession session,
JoinableTaskFactory joinableTaskFactory)
{
if (session is null)
{
throw new ArgumentNullException(nameof(session));
}
if (joinableTaskFactory is null)
{
throw new ArgumentNullException(nameof(joinableTaskFactory));
}
_session = session;
_joinableTaskFactory = joinableTaskFactory;
}
public async Task<Uri?> GetProjectPathAsync(Uri documentFilePath, CancellationToken cancellationToken)
{
if (documentFilePath is null)
@ -42,13 +28,13 @@ internal class DefaultProjectHierarchyProxy : IProjectHierarchyProxy, ICollabora
throw new ArgumentNullException(nameof(documentFilePath));
}
await _joinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
await _jtf.SwitchToMainThreadAsync(cancellationToken);
_openDocumentShell ??= _serviceProvider.GetService(typeof(SVsUIShellOpenDocument)) as IVsUIShellOpenDocument;
Assumes.Present(_openDocumentShell);
#pragma warning disable VSSDK006 // Check services exist
_openDocumentShell ??= ServiceProvider.GlobalProvider.GetService(typeof(SVsUIShellOpenDocument)) as IVsUIShellOpenDocument;
#pragma warning restore VSSDK006 // Check services exist
var hostDocumentFilePath = _session.ConvertSharedUriToLocalPath(documentFilePath);
var hr = _openDocumentShell!.IsDocumentInAProject(hostDocumentFilePath, out var hierarchy, out _, out _, out _);
var hr = _openDocumentShell.IsDocumentInAProject(hostDocumentFilePath, out var hierarchy, out _, out _, out _);
if (ErrorHandler.Succeeded(hr) && hierarchy != null)
{
ErrorHandler.ThrowOnFailure(((IVsProject)hierarchy).GetMkDocument((uint)VSConstants.VSITEMID.Root, out var path), VSConstants.E_NOTIMPL);

Просмотреть файл

@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.LiveShare.Razor.Host;
[ExportCollaborationService(
typeof(IProjectHierarchyProxy),
Name = nameof(IProjectHierarchyProxy),
Scope = SessionScope.Host,
Role = ServiceRole.RemoteService)]
[method: ImportingConstructor]
internal class ProjectHierarchyProxyFactory(
[Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider,
JoinableTaskContext joinableTaskContext) : ICollaborationServiceFactory
{
public Task<ICollaborationService> CreateServiceAsync(CollaborationSession session, CancellationToken cancellationToken)
{
var service = new ProjectHierarchyProxy(session, serviceProvider, joinableTaskContext.Factory);
return Task.FromResult<ICollaborationService>(service);
}
}

Просмотреть файл

@ -13,49 +13,28 @@ using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.LiveShare.Razor.Host;
internal class DefaultProjectSnapshotManagerProxy : IProjectSnapshotManagerProxy, ICollaborationService, IDisposable
internal class ProjectSnapshotManagerProxy : IProjectSnapshotManagerProxy, ICollaborationService, IDisposable
{
private readonly CollaborationSession _session;
private readonly ProjectSnapshotManagerDispatcher _dispatcher;
private readonly IProjectSnapshotManager _projectSnapshotManager;
private readonly JoinableTaskFactory _joinableTaskFactory;
private readonly JoinableTaskFactory _jtf;
private readonly AsyncSemaphore _latestStateSemaphore;
private bool _disposed;
private ProjectSnapshotManagerProxyState? _latestState;
// Internal for testing
internal JoinableTask? _processingChangedEventTestTask;
private JoinableTask? _processingChangedEventTestTask;
public DefaultProjectSnapshotManagerProxy(
public ProjectSnapshotManagerProxy(
CollaborationSession session,
ProjectSnapshotManagerDispatcher dispatcher,
IProjectSnapshotManager projectSnapshotManager,
JoinableTaskFactory joinableTaskFactory)
ProjectSnapshotManagerDispatcher dispatcher,
JoinableTaskFactory jtf)
{
if (session is null)
{
throw new ArgumentNullException(nameof(session));
}
if (dispatcher is null)
{
throw new ArgumentNullException(nameof(dispatcher));
}
if (projectSnapshotManager is null)
{
throw new ArgumentNullException(nameof(projectSnapshotManager));
}
if (joinableTaskFactory is null)
{
throw new ArgumentNullException(nameof(joinableTaskFactory));
}
_session = session;
_dispatcher = dispatcher;
_projectSnapshotManager = projectSnapshotManager;
_joinableTaskFactory = joinableTaskFactory;
_jtf = jtf;
_latestStateSemaphore = new AsyncSemaphore(initialCount: 1);
_projectSnapshotManager.Changed += ProjectSnapshotManager_Changed;
@ -81,8 +60,6 @@ internal class DefaultProjectSnapshotManagerProxy : IProjectSnapshotManagerProxy
public void Dispose()
{
_dispatcher.AssertDispatcherThread();
_projectSnapshotManager.Changed -= ProjectSnapshotManager_Changed;
_latestStateSemaphore.Dispose();
_disposed = true;
@ -91,9 +68,9 @@ internal class DefaultProjectSnapshotManagerProxy : IProjectSnapshotManagerProxy
// Internal for testing
internal async Task<IReadOnlyList<IProjectSnapshot>> GetLatestProjectsAsync()
{
if (!_joinableTaskFactory.Context.IsOnMainThread)
if (!_jtf.Context.IsOnMainThread)
{
await _joinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None);
await _jtf.SwitchToMainThreadAsync(CancellationToken.None);
}
return _projectSnapshotManager.GetProjects();
@ -148,11 +125,11 @@ internal class DefaultProjectSnapshotManagerProxy : IProjectSnapshotManagerProxy
return;
}
_processingChangedEventTestTask = _joinableTaskFactory.RunAsync(async () =>
_processingChangedEventTestTask = _jtf.RunAsync(async () =>
{
var projects = await GetLatestProjectsAsync();
await _joinableTaskFactory.SwitchToMainThreadAsync();
await _jtf.SwitchToMainThreadAsync();
var oldProjectProxy = await ConvertToProxyAsync(args.Older).ConfigureAwait(false);
var newProjectProxy = await ConvertToProxyAsync(args.Newer).ConfigureAwait(false);
@ -173,4 +150,12 @@ internal class DefaultProjectSnapshotManagerProxy : IProjectSnapshotManagerProxy
Changed?.Invoke(this, args);
}
internal TestAccessor GetTestAccessor()
=> new(this);
internal sealed class TestAccessor(ProjectSnapshotManagerProxy instance)
{
public JoinableTask? ProcessingChangedEventTestTask => instance._processingChangedEventTestTask;
}
}

Просмотреть файл

@ -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;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
@ -19,23 +18,18 @@ namespace Microsoft.VisualStudio.LiveShare.Razor.Host;
Scope = SessionScope.Host,
Role = ServiceRole.RemoteService)]
[method: ImportingConstructor]
internal class DefaultProjectSnapshotManagerProxyFactory(
internal class ProjectSnapshotManagerProxyFactory(
IProjectSnapshotManagerAccessor projectManagerAccessor,
ProjectSnapshotManagerDispatcher dispatcher,
JoinableTaskContext joinableTaskContext,
IProjectSnapshotManagerAccessor projectManagerAccessor) : ICollaborationServiceFactory
JoinableTaskContext joinableTaskContext) : ICollaborationServiceFactory
{
public Task<ICollaborationService> CreateServiceAsync(CollaborationSession session, CancellationToken cancellationToken)
{
if (session is null)
{
throw new ArgumentNullException(nameof(session));
}
var serializer = (JsonSerializer)session.GetService(typeof(JsonSerializer));
serializer.Converters.RegisterRazorLiveShareConverters();
var service = new DefaultProjectSnapshotManagerProxy(
session, dispatcher, projectManagerAccessor.Instance, joinableTaskContext.Factory);
var service = new ProjectSnapshotManagerProxy(
session, projectManagerAccessor.Instance, dispatcher, joinableTaskContext.Factory);
return Task.FromResult<ICollaborationService>(service);
}
}

Просмотреть файл

@ -7,7 +7,8 @@ using System.Threading.Tasks;
namespace Microsoft.VisualStudio.LiveShare.Razor;
internal interface IProjectHierarchyProxy
// This type must be a public interface in order to to be implemented as an RPC proxy by live share.
public interface IProjectHierarchyProxy
{
Task<Uri?> GetProjectPathAsync(Uri documentFilePath, CancellationToken cancellationToken);
}

Просмотреть файл

@ -7,9 +7,10 @@ using System.Threading.Tasks;
namespace Microsoft.VisualStudio.LiveShare.Razor;
internal interface IProjectSnapshotManagerProxy
// This type must be a public interface in order to to be implemented as an RPC proxy by live share.
public interface IProjectSnapshotManagerProxy
{
event EventHandler<ProjectChangeEventProxyArgs>? Changed;
event EventHandler<ProjectChangeEventProxyArgs> Changed;
Task<ProjectSnapshotManagerProxyState> GetProjectManagerStateAsync(CancellationToken cancellationToken);
}

Просмотреть файл

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Microsoft.VisualStudio.LiveShare.Razor;
// This type must be a public interface in order to properly advertise itself as part of the LiveShare ICollaborationService infrastructure.
// This type must be a public interface in order to to be implemented as an RPC proxy by live share.
public interface IRemoteHierarchyService : ICollaborationService
{
public Task<bool> HasCapabilityAsync(Uri pathOfFileInProject, string capability, CancellationToken cancellationToken);

Просмотреть файл

@ -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;
using System.ComponentModel.Composition;
using System.Threading;
using Microsoft.VisualStudio.Editor.Razor;
@ -11,29 +10,13 @@ using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.LiveShare.Razor;
[Export(typeof(ProjectCapabilityResolver))]
internal class LiveShareProjectCapabilityResolver : ProjectCapabilityResolver
[method: ImportingConstructor]
internal class LiveShareProjectCapabilityResolver(
ILiveShareSessionAccessor sessionAccessor,
JoinableTaskContext joinableTaskContext) : ProjectCapabilityResolver
{
private readonly LiveShareSessionAccessor _sessionAccessor;
private readonly JoinableTaskFactory _joinableTaskFactory;
[ImportingConstructor]
public LiveShareProjectCapabilityResolver(
LiveShareSessionAccessor sessionAccessor,
JoinableTaskContext joinableTaskContext)
{
if (sessionAccessor is null)
{
throw new ArgumentNullException(nameof(sessionAccessor));
}
if (joinableTaskContext is null)
{
throw new ArgumentNullException(nameof(joinableTaskContext));
}
_sessionAccessor = sessionAccessor;
_joinableTaskFactory = joinableTaskContext.Factory;
}
private readonly ILiveShareSessionAccessor _sessionAccessor = sessionAccessor;
private readonly JoinableTaskFactory _joinableTaskFactory = joinableTaskContext.Factory;
public override bool HasCapability(object project, string capability)
{

Просмотреть файл

@ -2,11 +2,20 @@
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor;
namespace Microsoft.VisualStudio.LiveShare.Razor;
internal sealed class ProjectChangeEventProxyArgs : EventArgs
// This type must be public because it is exposed by a public interface that is implemented as
// an RPC proxy by live share.
public sealed class ProjectChangeEventProxyArgs : EventArgs
{
public ProjectSnapshotHandleProxy? Older { get; }
public ProjectSnapshotHandleProxy? Newer { get; }
public ProjectProxyChangeKind Kind { get; }
public Uri ProjectFilePath { get; }
public Uri IntermediateOutputPath { get; }
public ProjectChangeEventProxyArgs(ProjectSnapshotHandleProxy? older, ProjectSnapshotHandleProxy? newer, ProjectProxyChangeKind kind)
{
if (older is null && newer is null)
@ -18,17 +27,7 @@ internal sealed class ProjectChangeEventProxyArgs : EventArgs
Newer = newer;
Kind = kind;
ProjectFilePath = older?.FilePath ?? newer!.FilePath;
IntermediateOutputPath = older?.IntermediateOutputPath ?? newer!.IntermediateOutputPath;
ProjectFilePath = older?.FilePath ?? newer.AssumeNotNull().FilePath;
IntermediateOutputPath = older?.IntermediateOutputPath ?? newer.AssumeNotNull().IntermediateOutputPath;
}
public ProjectSnapshotHandleProxy? Older { get; }
public ProjectSnapshotHandleProxy? Newer { get; }
public Uri ProjectFilePath { get; }
public Uri IntermediateOutputPath { get; }
public ProjectProxyChangeKind Kind { get; }
}

Просмотреть файл

@ -3,7 +3,9 @@
namespace Microsoft.VisualStudio.LiveShare.Razor;
internal enum ProjectProxyChangeKind
// This type must be public because it is exposed by a public interface that is implemented as
// an RPC proxy by live share.
public enum ProjectProxyChangeKind
{
ProjectAdded,
ProjectRemoved,

Просмотреть файл

@ -7,20 +7,23 @@ using Microsoft.AspNetCore.Razor.ProjectSystem;
namespace Microsoft.VisualStudio.LiveShare.Razor;
internal sealed class ProjectSnapshotHandleProxy
// This type must be public because it is exposed by a public interface that is implemented as
// an RPC proxy by live share. However, its properties and constructor are intentionally internal
// because they expose internal compiler APIs.
public sealed class ProjectSnapshotHandleProxy
{
public Uri FilePath { get; }
public Uri IntermediateOutputPath { get; }
public RazorConfiguration Configuration { get; }
public string? RootNamespace { get; }
public ProjectWorkspaceState? ProjectWorkspaceState { get; }
internal Uri FilePath { get; }
internal Uri IntermediateOutputPath { get; }
internal RazorConfiguration Configuration { get; }
internal string? RootNamespace { get; }
internal ProjectWorkspaceState ProjectWorkspaceState { get; }
public ProjectSnapshotHandleProxy(
internal ProjectSnapshotHandleProxy(
Uri filePath,
Uri intermediateOutputPath,
RazorConfiguration configuration,
string? rootNamespace,
ProjectWorkspaceState? projectWorkspaceState)
ProjectWorkspaceState projectWorkspaceState)
{
FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
IntermediateOutputPath = intermediateOutputPath ?? throw new ArgumentNullException(nameof(intermediateOutputPath));

Просмотреть файл

@ -6,7 +6,9 @@ using System.Collections.Generic;
namespace Microsoft.VisualStudio.LiveShare.Razor;
internal sealed class ProjectSnapshotManagerProxyState(IReadOnlyList<ProjectSnapshotHandleProxy> projectHandles)
// This type must be public because it is exposed by a public interface that is implemented as
// an RPC proxy by live share.
public sealed class ProjectSnapshotManagerProxyState(IReadOnlyList<ProjectSnapshotHandleProxy> projectHandles)
{
public IReadOnlyList<ProjectSnapshotHandleProxy> ProjectHandles { get; } = projectHandles ?? throw new ArgumentNullException(nameof(projectHandles));
}

Просмотреть файл

@ -1,3 +1,23 @@
#nullable enable
Microsoft.VisualStudio.LiveShare.Razor.IProjectHierarchyProxy
Microsoft.VisualStudio.LiveShare.Razor.IProjectHierarchyProxy.GetProjectPathAsync(System.Uri! documentFilePath, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<System.Uri?>!
Microsoft.VisualStudio.LiveShare.Razor.IProjectSnapshotManagerProxy
Microsoft.VisualStudio.LiveShare.Razor.IProjectSnapshotManagerProxy.Changed -> System.EventHandler<Microsoft.VisualStudio.LiveShare.Razor.ProjectChangeEventProxyArgs!>!
Microsoft.VisualStudio.LiveShare.Razor.IProjectSnapshotManagerProxy.GetProjectManagerStateAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotManagerProxyState!>!
Microsoft.VisualStudio.LiveShare.Razor.IRemoteHierarchyService
Microsoft.VisualStudio.LiveShare.Razor.IRemoteHierarchyService.HasCapabilityAsync(System.Uri! pathOfFileInProject, string! capability, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<bool>!
Microsoft.VisualStudio.LiveShare.Razor.ProjectChangeEventProxyArgs
Microsoft.VisualStudio.LiveShare.Razor.ProjectChangeEventProxyArgs.IntermediateOutputPath.get -> System.Uri!
Microsoft.VisualStudio.LiveShare.Razor.ProjectChangeEventProxyArgs.Kind.get -> Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind
Microsoft.VisualStudio.LiveShare.Razor.ProjectChangeEventProxyArgs.Newer.get -> Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotHandleProxy?
Microsoft.VisualStudio.LiveShare.Razor.ProjectChangeEventProxyArgs.Older.get -> Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotHandleProxy?
Microsoft.VisualStudio.LiveShare.Razor.ProjectChangeEventProxyArgs.ProjectChangeEventProxyArgs(Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotHandleProxy? older, Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotHandleProxy? newer, Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind kind) -> void
Microsoft.VisualStudio.LiveShare.Razor.ProjectChangeEventProxyArgs.ProjectFilePath.get -> System.Uri!
Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind
Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind.ProjectAdded = 0 -> Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind
Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind.ProjectChanged = 2 -> Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind
Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind.ProjectRemoved = 1 -> Microsoft.VisualStudio.LiveShare.Razor.ProjectProxyChangeKind
Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotHandleProxy
Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotManagerProxyState
Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotManagerProxyState.ProjectHandles.get -> System.Collections.Generic.IReadOnlyList<Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotHandleProxy!>!
Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotManagerProxyState.ProjectSnapshotManagerProxyState(System.Collections.Generic.IReadOnlyList<Microsoft.VisualStudio.LiveShare.Razor.ProjectSnapshotHandleProxy!>! projectHandles) -> void

Просмотреть файл

@ -10,26 +10,10 @@ using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.LiveShare.Razor;
internal class RemoteHierarchyService : IRemoteHierarchyService
internal class RemoteHierarchyService(CollaborationSession session, JoinableTaskFactory jtf) : IRemoteHierarchyService
{
private readonly CollaborationSession _session;
private readonly JoinableTaskFactory _joinableTaskFactory;
internal RemoteHierarchyService(CollaborationSession session, JoinableTaskFactory joinableTaskFactory)
{
if (session is null)
{
throw new ArgumentNullException(nameof(session));
}
if (joinableTaskFactory is null)
{
throw new ArgumentNullException(nameof(joinableTaskFactory));
}
_session = session;
_joinableTaskFactory = joinableTaskFactory;
}
private readonly CollaborationSession _session = session;
private readonly JoinableTaskFactory _jtf = jtf;
public async Task<bool> HasCapabilityAsync(Uri pathOfFileInProject, string capability, CancellationToken cancellationToken)
{
@ -43,7 +27,7 @@ internal class RemoteHierarchyService : IRemoteHierarchyService
throw new ArgumentNullException(nameof(capability));
}
await _joinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
await _jtf.SwitchToMainThreadAsync(cancellationToken);
var hostPathOfFileInProject = _session.ConvertSharedUriToLocalPath(pathOfFileInProject);
if (ServiceProvider.GlobalProvider.GetService(typeof(SVsUIShellOpenDocument)) is not IVsUIShellOpenDocument vsUIShellOpenDocument)

Просмотреть файл

@ -1,9 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Threading;
using Task = System.Threading.Tasks.Task;
namespace Microsoft.VisualStudio.LiveShare.Razor;
@ -17,10 +18,11 @@ namespace Microsoft.VisualStudio.LiveShare.Razor;
Name = nameof(IRemoteHierarchyService),
Scope = SessionScope.Host,
Role = ServiceRole.RemoteService)]
internal sealed class RemoteHierarchyServiceFactory : ICollaborationServiceFactory
[method: ImportingConstructor]
internal sealed class RemoteHierarchyServiceFactory(JoinableTaskContext joinableTaskContext) : ICollaborationServiceFactory
{
public Task<ICollaborationService> CreateServiceAsync(CollaborationSession session, CancellationToken cancellationToken)
{
return Task.FromResult<ICollaborationService>(new RemoteHierarchyService(session, ThreadHelper.JoinableTaskFactory));
return Task.FromResult<ICollaborationService>(new RemoteHierarchyService(session, joinableTaskContext.Factory));
}
}

Просмотреть файл

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Serialization.Json;
namespace Microsoft.VisualStudio.LiveShare.Razor.Serialization;
@ -15,7 +16,7 @@ internal class ProjectSnapshotHandleProxyJsonConverter : ObjectJsonConverter<Pro
var intermediateOutputPath = reader.ReadNonNullUri(nameof(ProjectSnapshotHandleProxy.IntermediateOutputPath));
var configuration = reader.ReadNonNullObject(nameof(ProjectSnapshotHandleProxy.Configuration), ObjectReaders.ReadConfigurationFromProperties);
var rootNamespace = reader.ReadStringOrNull(nameof(ProjectSnapshotHandleProxy.RootNamespace));
var projectWorkspaceState = reader.ReadObjectOrNull(nameof(ProjectSnapshotHandleProxy.ProjectWorkspaceState), ObjectReaders.ReadProjectWorkspaceStateFromProperties);
var projectWorkspaceState = reader.ReadObjectOrNull(nameof(ProjectSnapshotHandleProxy.ProjectWorkspaceState), ObjectReaders.ReadProjectWorkspaceStateFromProperties) ?? ProjectWorkspaceState.Default;
return new(filePath, intermediateOutputPath, configuration, rootNamespace, projectWorkspaceState);
}

Просмотреть файл

@ -1,50 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
#nullable disable
using System;
using Microsoft.AspNetCore.Razor.Test.Common;
using Moq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
public class DefaultProxyAccessorTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput)
{
[Fact]
public void GetProjectHierarchyProxy_Caches()
{
// Arrange
var proxy = Mock.Of<IProjectHierarchyProxy>(MockBehavior.Strict);
var proxyAccessor = new TestProxyAccessor<IProjectHierarchyProxy>(proxy);
// Act
var proxy1 = proxyAccessor.GetProjectHierarchyProxy();
var proxy2 = proxyAccessor.GetProjectHierarchyProxy();
// Assert
Assert.Same(proxy1, proxy2);
}
private class TestProxyAccessor<TTestProxy> : DefaultProxyAccessor where TTestProxy : class
{
private readonly TTestProxy _proxy;
public TestProxyAccessor(TTestProxy proxy)
{
_proxy = proxy;
}
internal override TProxy CreateServiceProxy<TProxy>()
{
if (typeof(TProxy) == typeof(TTestProxy))
{
return _proxy as TProxy;
}
throw new InvalidOperationException("The proxy accessor was called with unexpected arguments.");
}
}
}

Просмотреть файл

@ -1,47 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
#nullable disable
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.VisualStudio.LiveShare.Razor.Test;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Threading;
using Moq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
public class GuestProjectPathProviderTest : ToolingTestBase
public class GuestProjectPathProviderTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput)
{
private readonly LiveShareSessionAccessor _sessionAccessor;
public GuestProjectPathProviderTest(ITestOutputHelper testOutput)
: base(testOutput)
{
var collabSession = new TestCollaborationSession(isHost: false);
_sessionAccessor = Mock.Of<LiveShareSessionAccessor>(
a => a.IsGuestSessionActive == true && a.Session == collabSession,
MockBehavior.Strict);
}
[Fact]
[UIFact]
public void TryGetProjectPath_GuestSessionNotActive_ReturnsFalse()
{
// Arrange
var sessionAccessor = Mock.Of<LiveShareSessionAccessor>(accessor => accessor.IsGuestSessionActive == false, MockBehavior.Strict);
var textBuffer = Mock.Of<ITextBuffer>(MockBehavior.Strict);
var textDocument = Mock.Of<ITextDocument>(MockBehavior.Strict);
var textDocumentFactory = Mock.Of<ITextDocumentFactoryService>(factory => factory.TryGetTextDocument(textBuffer, out textDocument) == true, MockBehavior.Strict);
var projectPathProvider = new TestGuestProjectPathProvider(
new Uri("vsls:/path/project.csproj"),
var sessionAccessor = StrictMock.Of<ILiveShareSessionAccessor>(a =>
a.IsGuestSessionActive == false);
var textBuffer = StrictMock.Of<ITextBuffer>();
var textDocument = StrictMock.Of<ITextDocument>();
var textDocumentFactory = StrictMock.Of<ITextDocumentFactoryService>(f =>
f.TryGetTextDocument(textBuffer, out textDocument) == true);
var projectPathProvider = new GuestProjectPathProvider(
JoinableTaskContext,
textDocumentFactory,
Mock.Of<ProxyAccessor>(MockBehavior.Strict),
StrictMock.Of<IProxyAccessor>(),
sessionAccessor);
// Act
@ -52,18 +38,27 @@ public class GuestProjectPathProviderTest : ToolingTestBase
Assert.Null(filePath);
}
[Fact]
[UIFact]
public void TryGetProjectPath_NoTextDocument_ReturnsFalse()
{
// Arrange
var textBuffer = Mock.Of<ITextBuffer>(MockBehavior.Strict);
var textDocumentFactoryService = new Mock<ITextDocumentFactoryService>(MockBehavior.Strict);
textDocumentFactoryService.Setup(s => s.TryGetTextDocument(It.IsAny<ITextBuffer>(), out It.Ref<ITextDocument>.IsAny)).Returns(false);
var collaborationSession = StrictMock.Of<CollaborationSession>();
var sessionAccessor = StrictMock.Of<ILiveShareSessionAccessor>(a =>
a.IsGuestSessionActive == true &&
a.Session == collaborationSession);
var textBuffer = StrictMock.Of<ITextBuffer>();
var textDocumentFactoryServiceMock = new StrictMock<ITextDocumentFactoryService>();
textDocumentFactoryServiceMock
.Setup(s => s.TryGetTextDocument(It.IsAny<ITextBuffer>(), out It.Ref<ITextDocument>.IsAny))
.Returns(false);
var projectPathProvider = new GuestProjectPathProvider(
JoinableTaskContext,
textDocumentFactoryService.Object,
Mock.Of<ProxyAccessor>(MockBehavior.Strict),
_sessionAccessor);
textDocumentFactoryServiceMock.Object,
StrictMock.Of<IProxyAccessor>(),
sessionAccessor);
// Act
var result = projectPathProvider.TryGetProjectPath(textBuffer, out var filePath);
@ -73,19 +68,46 @@ public class GuestProjectPathProviderTest : ToolingTestBase
Assert.Null(filePath);
}
[Fact]
[UIFact]
public void TryGetProjectPath_NullHostProjectPath_ReturnsFalse()
{
// Arrange
var textBuffer = Mock.Of<ITextBuffer>(MockBehavior.Strict);
var textDocument = Mock.Of<ITextDocument>(MockBehavior.Strict);
var textDocumentFactory = Mock.Of<ITextDocumentFactoryService>(factory => factory.TryGetTextDocument(textBuffer, out textDocument) == true, MockBehavior.Strict);
var projectPathProvider = new TestGuestProjectPathProvider(
null,
var documentFilePath = "/path/to/document.razor";
var documentFilePathUri = new Uri("vsls:" + documentFilePath);
var projectFilePath = "/path/to/project.razor";
var projectFilePathUri = new Uri("vsls:" + projectFilePath);
var collaborationSessionMock = new StrictMock<CollaborationSession>();
collaborationSessionMock
.Setup(x => x.ConvertLocalPathToSharedUri(documentFilePath))
.Returns(documentFilePathUri);
collaborationSessionMock
.Setup(x => x.ConvertSharedUriToLocalPath(projectFilePathUri))
.Returns(projectFilePath);
var sessionAccessor = StrictMock.Of<ILiveShareSessionAccessor>(a =>
a.IsGuestSessionActive == true &&
a.Session == collaborationSessionMock.Object);
var textBuffer = StrictMock.Of<ITextBuffer>();
var textDocument = StrictMock.Of<ITextDocument>(d =>
d.FilePath == documentFilePath);
var textDocumentFactory = StrictMock.Of<ITextDocumentFactoryService>(f =>
f.TryGetTextDocument(textBuffer, out textDocument) == true);
var proxy = new StrictMock<IProjectHierarchyProxy>();
proxy
.Setup(x => x.GetProjectPathAsync(documentFilePathUri, CancellationToken.None))
.ReturnsAsync((Uri?)null);
var proxyAccessor = StrictMock.Of<IProxyAccessor>(a =>
a.GetProjectHierarchyProxy() == proxy.Object);
var projectPathProvider = new GuestProjectPathProvider(
JoinableTaskContext,
textDocumentFactory,
Mock.Of<ProxyAccessor>(MockBehavior.Strict),
_sessionAccessor);
proxyAccessor,
sessionAccessor);
// Act
var result = projectPathProvider.TryGetProjectPath(textBuffer, out var filePath);
@ -95,76 +117,55 @@ public class GuestProjectPathProviderTest : ToolingTestBase
Assert.Null(filePath);
}
[Fact]
[UIFact]
public void TryGetProjectPath_ValidHostProjectPath_ReturnsTrueWithGuestNormalizedPath()
{
// Arrange
var textBuffer = Mock.Of<ITextBuffer>(MockBehavior.Strict);
var textDocument = Mock.Of<ITextDocument>(MockBehavior.Strict);
var textDocumentFactory = Mock.Of<ITextDocumentFactoryService>(factory => factory.TryGetTextDocument(textBuffer, out textDocument) == true, MockBehavior.Strict);
var expectedProjectPath = "/guest/path/project.csproj";
var projectPathProvider = new TestGuestProjectPathProvider(
new Uri("vsls:/path/project.csproj"),
var documentFilePath = "/path/to/document.razor";
var documentFilePathUri = new Uri("vsls:" + documentFilePath);
var projectFilePath = "/path/to/project.csproj";
var projectFilePathUri = new Uri("vsls:" + projectFilePath);
var collaborationSessionMock = new StrictMock<CollaborationSession>();
collaborationSessionMock
.Setup(x => x.ConvertLocalPathToSharedUri(documentFilePath))
.Returns(documentFilePathUri);
collaborationSessionMock
.Setup(x => x.ConvertSharedUriToLocalPath(projectFilePathUri))
.Returns(projectFilePath);
var sessionAccessor = StrictMock.Of<ILiveShareSessionAccessor>(a =>
a.IsGuestSessionActive == true &&
a.Session == collaborationSessionMock.Object);
var textBuffer = StrictMock.Of<ITextBuffer>();
var textDocument = StrictMock.Of<ITextDocument>(d =>
d.FilePath == documentFilePath);
var textDocumentFactory = StrictMock.Of<ITextDocumentFactoryService>(f =>
f.TryGetTextDocument(textBuffer, out textDocument) == true);
var proxy = new StrictMock<IProjectHierarchyProxy>();
proxy
.Setup(x => x.GetProjectPathAsync(documentFilePathUri, CancellationToken.None))
.ReturnsAsync(projectFilePathUri)
.Verifiable();
var proxyAccessor = StrictMock.Of<IProxyAccessor>(a =>
a.GetProjectHierarchyProxy() == proxy.Object);
var projectPathProvider = new GuestProjectPathProvider(
JoinableTaskContext,
textDocumentFactory,
Mock.Of<ProxyAccessor>(MockBehavior.Strict),
_sessionAccessor);
proxyAccessor,
sessionAccessor);
// Act
var result = projectPathProvider.TryGetProjectPath(textBuffer, out var filePath);
// Assert
Assert.True(result);
Assert.Equal(expectedProjectPath, filePath);
}
Assert.Equal(projectFilePath, filePath);
[Fact]
public void GetHostProjectPath_AsksProxyForProjectPathAsync()
{
// Arrange
var expectedGuestFilePath = "/guest/path/index.cshtml";
var expectedHostFilePath = new Uri("vsls:/path/index.cshtml");
var expectedHostProjectPath = new Uri("vsls:/path/project.csproj");
var collabSession = new TestCollaborationSession(isHost: true);
var sessionAccessor = Mock.Of<LiveShareSessionAccessor>(accessor => accessor.IsGuestSessionActive == true && accessor.Session == collabSession, MockBehavior.Strict);
var proxy = Mock.Of<IProjectHierarchyProxy>(
p => p.GetProjectPathAsync(expectedHostFilePath, CancellationToken.None) == Task.FromResult(expectedHostProjectPath),
MockBehavior.Strict);
var proxyAccessor = Mock.Of<ProxyAccessor>(
accessor => accessor.GetProjectHierarchyProxy() == proxy,
MockBehavior.Strict);
var textDocument = Mock.Of<ITextDocument>(
document => document.FilePath == expectedGuestFilePath,
MockBehavior.Strict);
var projectPathProvider = new GuestProjectPathProvider(
JoinableTaskContext,
Mock.Of<ITextDocumentFactoryService>(MockBehavior.Strict),
proxyAccessor,
sessionAccessor);
// Act
var hostProjectPath = projectPathProvider.GetHostProjectPath(textDocument);
// Assert
Assert.Equal(expectedHostProjectPath, hostProjectPath);
}
private class TestGuestProjectPathProvider : GuestProjectPathProvider
{
private readonly Uri _hostProjectPath;
public TestGuestProjectPathProvider(
Uri hostProjectPath,
JoinableTaskContext joinableTaskContext,
ITextDocumentFactoryService textDocumentFactory,
ProxyAccessor proxyAccessor,
LiveShareSessionAccessor liveShareSessionAccessor)
: base(joinableTaskContext, textDocumentFactory, proxyAccessor, liveShareSessionAccessor)
{
_hostProjectPath = hostProjectPath;
}
internal override Uri GetHostProjectPath(ITextDocument textDocument) => _hostProjectPath;
proxy.Verify();
}
}

Просмотреть файл

@ -3,11 +3,11 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.Editor;
using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -30,9 +30,11 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
{
_sessionContext = new TestCollaborationSession(isHost: false);
var projectManager = new TestProjectSnapshotManager(ProjectEngineFactoryProvider, new TestProjectSnapshotManagerDispatcher());
var projectManager = new TestProjectSnapshotManager(
ProjectEngineFactoryProvider,
Dispatcher);
var projectManagerAccessorMock = new Mock<IProjectSnapshotManagerAccessor>(MockBehavior.Strict);
var projectManagerAccessorMock = new StrictMock<IProjectSnapshotManagerAccessor>();
projectManagerAccessorMock
.SetupGet(x => x.Instance)
.Returns(projectManager);
@ -43,7 +45,7 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build()));
}
[Fact]
[UIFact]
public async Task InitializeAsync_RetrievesHostProjectManagerStateAndInitializesGuestManager()
{
// Arrange
@ -53,16 +55,19 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
RazorConfiguration.Default,
"project",
_projectWorkspaceStateWithTagHelpers);
var state = new ProjectSnapshotManagerProxyState(new[] { projectHandle });
var hostProjectManagerProxy = Mock.Of<IProjectSnapshotManagerProxy>(
proxy => proxy.GetProjectManagerStateAsync(It.IsAny<CancellationToken>()) == Task.FromResult(state), MockBehavior.Strict);
var state = new ProjectSnapshotManagerProxyState([projectHandle]);
var hostProjectManagerProxyMock = new StrictMock<IProjectSnapshotManagerProxy>();
hostProjectManagerProxyMock
.Setup(x => x.GetProjectManagerStateAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(state);
var synchronizationService = new ProjectSnapshotSynchronizationService(
JoinableTaskFactory,
_sessionContext,
hostProjectManagerProxy,
hostProjectManagerProxyMock.Object,
_projectManagerAccessor,
Dispatcher,
ErrorReporter,
Dispatcher);
JoinableTaskFactory);
// Act
await synchronizationService.InitializeAsync(DisposalToken);
@ -82,7 +87,7 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
}
}
[Fact]
[UIFact]
public async Task UpdateGuestProjectManager_ProjectAdded()
{
// Arrange
@ -93,12 +98,12 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
"project",
_projectWorkspaceStateWithTagHelpers);
var synchronizationService = new ProjectSnapshotSynchronizationService(
JoinableTaskFactory,
_sessionContext,
Mock.Of<IProjectSnapshotManagerProxy>(MockBehavior.Strict),
StrictMock.Of<IProjectSnapshotManagerProxy>(),
_projectManagerAccessor,
Dispatcher,
ErrorReporter,
Dispatcher);
JoinableTaskFactory);
var args = new ProjectChangeEventProxyArgs(older: null, newHandle, ProjectProxyChangeKind.ProjectAdded);
// Act
@ -119,7 +124,7 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
}
}
[Fact]
[UIFact]
public async Task UpdateGuestProjectManager_ProjectRemoved()
{
// Arrange
@ -128,14 +133,14 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
new Uri("vsls:/path/obj"),
RazorConfiguration.Default,
"project",
projectWorkspaceState: null);
ProjectWorkspaceState.Default);
var synchronizationService = new ProjectSnapshotSynchronizationService(
JoinableTaskFactory,
_sessionContext,
Mock.Of<IProjectSnapshotManagerProxy>(MockBehavior.Strict),
StrictMock.Of<IProjectSnapshotManagerProxy>(),
_projectManagerAccessor,
Dispatcher,
ErrorReporter,
Dispatcher);
JoinableTaskFactory);
var hostProject = new HostProject("/guest/path/project.csproj", "/guest/path/obj", RazorConfiguration.Default, "project");
await Dispatcher.RunOnDispatcherThreadAsync(
@ -152,7 +157,7 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
Assert.Empty(projects);
}
[Fact]
[UIFact]
public async Task UpdateGuestProjectManager_ProjectChanged_ConfigurationChange()
{
// Arrange
@ -161,8 +166,8 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
new Uri("vsls:/path/obj"),
RazorConfiguration.Default,
"project",
projectWorkspaceState: null);
var newConfiguration = RazorConfiguration.Create(RazorLanguageVersion.Version_1_0, "Custom-1.0", Enumerable.Empty<RazorExtension>());
ProjectWorkspaceState.Default);
var newConfiguration = RazorConfiguration.Create(RazorLanguageVersion.Version_1_0, "Custom-1.0", []);
var newHandle = new ProjectSnapshotHandleProxy(
oldHandle.FilePath,
oldHandle.IntermediateOutputPath,
@ -170,12 +175,12 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
oldHandle.RootNamespace,
oldHandle.ProjectWorkspaceState);
var synchronizationService = new ProjectSnapshotSynchronizationService(
JoinableTaskFactory,
_sessionContext,
Mock.Of<IProjectSnapshotManagerProxy>(MockBehavior.Strict),
StrictMock.Of<IProjectSnapshotManagerProxy>(),
_projectManagerAccessor,
Dispatcher,
ErrorReporter,
Dispatcher);
JoinableTaskFactory);
var hostProject = new HostProject("/guest/path/project.csproj", "/guest/path/obj", RazorConfiguration.Default, "project");
await Dispatcher.RunOnDispatcherThreadAsync(() =>
{
@ -197,7 +202,7 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
Assert.Empty(await project.GetTagHelpersAsync(CancellationToken.None));
}
[Fact]
[UIFact]
public async Task UpdateGuestProjectManager_ProjectChanged_ProjectWorkspaceStateChange()
{
// Arrange
@ -215,12 +220,12 @@ public class ProjectSnapshotSynchronizationServiceTest : ProjectSnapshotManagerD
oldHandle.RootNamespace,
newProjectWorkspaceState);
var synchronizationService = new ProjectSnapshotSynchronizationService(
JoinableTaskFactory,
_sessionContext,
Mock.Of<IProjectSnapshotManagerProxy>(MockBehavior.Strict),
StrictMock.Of<IProjectSnapshotManagerProxy>(),
_projectManagerAccessor,
Dispatcher,
ErrorReporter,
Dispatcher);
JoinableTaskFactory);
var hostProject = new HostProject("/guest/path/project.csproj", "/guest/path/obj", RazorConfiguration.Default, "project");
await Dispatcher.RunOnDispatcherThreadAsync(() =>
{

Просмотреть файл

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Threading;
using Microsoft.AspNetCore.Razor.Test.Common;
using Moq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
public class ProxyAccessorTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput)
{
[UIFact]
public void GetProjectHierarchyProxy_Caches()
{
// Arrange
var projectHierarchyProxy = StrictMock.Of<IProjectHierarchyProxy>();
var collaborationSessionMock = new StrictMock<CollaborationSession>();
collaborationSessionMock
.Setup(x => x.GetRemoteServiceAsync<IProjectHierarchyProxy>(typeof(IProjectHierarchyProxy).Name, CancellationToken.None))
.ReturnsAsync(projectHierarchyProxy);
var liveShareSessionAccessorMock = new StrictMock<ILiveShareSessionAccessor>();
liveShareSessionAccessorMock
.SetupGet(x => x.Session)
.Returns(collaborationSessionMock.Object);
var proxyAccessor = new ProxyAccessor(liveShareSessionAccessorMock.Object, JoinableTaskContext);
// Act
var proxy1 = proxyAccessor.GetProjectHierarchyProxy();
var proxy2 = proxyAccessor.GetProjectHierarchyProxy();
// Assert
Assert.Same(proxy1, proxy2);
}
}

Просмотреть файл

@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
#nullable disable
using System;
using System.Threading;
using System.Threading.Tasks;
@ -13,32 +11,28 @@ using Xunit.Abstractions;
namespace Microsoft.VisualStudio.LiveShare.Razor.Guest;
public class RazorGuestInitializationServiceTest : ToolingTestBase
public class RazorGuestInitializationServiceTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput)
{
private readonly DefaultLiveShareSessionAccessor _liveShareSessionAccessor;
public RazorGuestInitializationServiceTest(ITestOutputHelper testOutput)
: base(testOutput)
{
_liveShareSessionAccessor = new DefaultLiveShareSessionAccessor();
}
private readonly LiveShareSessionAccessor _liveShareSessionAccessor = new();
[Fact]
public async Task CreateServiceAsync_StartsViewImportsCopy()
{
// Arrange
var service = new RazorGuestInitializationService(_liveShareSessionAccessor);
var session = new Mock<CollaborationSession>(MockBehavior.Strict);
session.Setup(s => s.ListRootsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(Array.Empty<Uri>())
var serviceAccessor = service.GetTestAccessor();
var session = new StrictMock<CollaborationSession>();
session
.Setup(s => s.ListRootsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync([])
.Verifiable();
// Act
await service.CreateServiceAsync(session.Object, default);
// Assert
Assert.NotNull(service._viewImportsCopyTask);
await service._viewImportsCopyTask;
Assert.NotNull(serviceAccessor.ViewImportsCopyTask);
await serviceAccessor.ViewImportsCopyTask;
session.VerifyAll();
}
@ -48,11 +42,13 @@ public class RazorGuestInitializationServiceTest : ToolingTestBase
{
// Arrange
var service = new RazorGuestInitializationService(_liveShareSessionAccessor);
var session = new Mock<CollaborationSession>(MockBehavior.Strict);
var serviceAccessor = service.GetTestAccessor();
var session = new StrictMock<CollaborationSession>();
using var disposedServiceGate = new ManualResetEventSlim();
var disposedService = false;
IDisposable sessionService = null;
session.Setup(s => s.ListRootsAsync(It.IsAny<CancellationToken>()))
IDisposable? sessionService = null;
session
.Setup(s => s.ListRootsAsync(It.IsAny<CancellationToken>()))
.Returns<CancellationToken>((cancellationToken) => Task.Run(() =>
{
cancellationToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(5));
@ -64,7 +60,7 @@ public class RazorGuestInitializationServiceTest : ToolingTestBase
return Array.Empty<Uri>();
}))
.Verifiable();
sessionService = (IDisposable)await service.CreateServiceAsync(session.Object, default);
sessionService = (IDisposable)await service.CreateServiceAsync(session.Object, DisposalToken);
// Act
sessionService.Dispose();
@ -72,8 +68,8 @@ public class RazorGuestInitializationServiceTest : ToolingTestBase
disposedServiceGate.Set();
// Assert
Assert.NotNull(service._viewImportsCopyTask);
await service._viewImportsCopyTask;
Assert.NotNull(serviceAccessor.ViewImportsCopyTask);
await serviceAccessor.ViewImportsCopyTask;
session.VerifyAll();
}
@ -83,10 +79,12 @@ public class RazorGuestInitializationServiceTest : ToolingTestBase
{
// Arrange
var service = new RazorGuestInitializationService(_liveShareSessionAccessor);
var session = new Mock<CollaborationSession>(MockBehavior.Strict);
var serviceAccessor = service.GetTestAccessor();
var session = new StrictMock<CollaborationSession>();
using var cts = new CancellationTokenSource();
IDisposable sessionService = null;
session.Setup(s => s.ListRootsAsync(It.IsAny<CancellationToken>()))
IDisposable? sessionService = null;
session
.Setup(s => s.ListRootsAsync(It.IsAny<CancellationToken>()))
.Returns<CancellationToken>((cancellationToken) => Task.Run(() =>
{
cancellationToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(3));
@ -101,8 +99,8 @@ public class RazorGuestInitializationServiceTest : ToolingTestBase
cts.Cancel();
// Assert
Assert.NotNull(service._viewImportsCopyTask);
await service._viewImportsCopyTask;
Assert.NotNull(serviceAccessor.ViewImportsCopyTask);
await serviceAccessor.ViewImportsCopyTask;
session.VerifyAll();
}
@ -112,10 +110,12 @@ public class RazorGuestInitializationServiceTest : ToolingTestBase
{
// Arrange
var service = new RazorGuestInitializationService(_liveShareSessionAccessor);
var session = new Mock<CollaborationSession>(MockBehavior.Strict);
var serviceAcessor = service.GetTestAccessor();
var session = new StrictMock<CollaborationSession>();
using var cts = new CancellationTokenSource();
IDisposable sessionService = null;
session.Setup(s => s.ListRootsAsync(It.IsAny<CancellationToken>()))
IDisposable? sessionService = null;
session
.Setup(s => s.ListRootsAsync(It.IsAny<CancellationToken>()))
.Returns<CancellationToken>((cancellationToken) => Task.Run(() =>
{
cancellationToken.WaitHandle.WaitOne(TimeSpan.FromSeconds(3));
@ -131,8 +131,8 @@ public class RazorGuestInitializationServiceTest : ToolingTestBase
cts.Cancel();
// Assert
Assert.NotNull(service._viewImportsCopyTask);
await service._viewImportsCopyTask;
Assert.NotNull(serviceAcessor.ViewImportsCopyTask);
await serviceAcessor.ViewImportsCopyTask;
session.VerifyAll();
}

Просмотреть файл

@ -1,229 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
#nullable disable
using System;
using System.Collections.Immutable;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.ProjectEngineHost;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Test.Common.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.LiveShare.Razor.Test;
using Microsoft.VisualStudio.Threading;
using Moq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.LiveShare.Razor.Host;
public class DefaultProjectSnapshotManagerProxyTest : ProjectSnapshotManagerDispatcherTestBase
{
private readonly IProjectSnapshot _projectSnapshot1;
private readonly IProjectSnapshot _projectSnapshot2;
public DefaultProjectSnapshotManagerProxyTest(ITestOutputHelper testOutput)
: base(testOutput)
{
var projectEngineFactoryProvider = Mock.Of<IProjectEngineFactoryProvider>(MockBehavior.Strict);
var projectWorkspaceState1 = ProjectWorkspaceState.Create(ImmutableArray.Create(
TagHelperDescriptorBuilder.Create("test1", "TestAssembly1").Build()));
_projectSnapshot1 = new ProjectSnapshot(
ProjectState.Create(
projectEngineFactoryProvider,
new HostProject("/host/path/to/project1.csproj", "/host/path/to/obj", RazorConfiguration.Default, "project1"),
projectWorkspaceState1));
var projectWorkspaceState2 = ProjectWorkspaceState.Create(ImmutableArray.Create(
TagHelperDescriptorBuilder.Create("test2", "TestAssembly2").Build()));
_projectSnapshot2 = new ProjectSnapshot(
ProjectState.Create(
projectEngineFactoryProvider,
new HostProject("/host/path/to/project2.csproj", "/host/path/to/obj", RazorConfiguration.Default, "project2"),
projectWorkspaceState2));
}
[Fact]
public async Task CalculateUpdatedStateAsync_ReturnsStateForAllProjects()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1, _projectSnapshot2);
using var proxy = new DefaultProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
Dispatcher,
projectSnapshotManager,
JoinableTaskFactory);
// Act
var state = await JoinableTaskFactory.RunAsync(() => proxy.CalculateUpdatedStateAsync(projectSnapshotManager.GetProjects()));
// Assert
var project1TagHelpers = await _projectSnapshot1.GetTagHelpersAsync(CancellationToken.None);
var project2TagHelpers = await _projectSnapshot2.GetTagHelpersAsync(CancellationToken.None);
Assert.Collection(
state.ProjectHandles,
handle =>
{
Assert.Equal("vsls:/path/to/project1.csproj", handle.FilePath.ToString());
Assert.Equal<TagHelperDescriptor>(project1TagHelpers, handle.ProjectWorkspaceState.TagHelpers);
},
handle =>
{
Assert.Equal("vsls:/path/to/project2.csproj", handle.FilePath.ToString());
Assert.Equal<TagHelperDescriptor>(project2TagHelpers, handle.ProjectWorkspaceState.TagHelpers);
});
}
[Fact]
public async Task Changed_TriggersOnSnapshotManagerChanged()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1);
using var proxy = new DefaultProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
Dispatcher,
projectSnapshotManager,
JoinableTaskFactory);
var changedArgs = new ProjectChangeEventArgs(_projectSnapshot1, _projectSnapshot1, ProjectChangeKind.ProjectChanged);
var called = false;
proxy.Changed += (sender, args) =>
{
called = true;
Assert.Equal($"vsls:/path/to/project1.csproj", args.ProjectFilePath.ToString());
Assert.Equal(ProjectProxyChangeKind.ProjectChanged, args.Kind);
Assert.Equal("vsls:/path/to/project1.csproj", args.Newer.FilePath.ToString());
};
// Act
projectSnapshotManager.TriggerChanged(changedArgs);
await proxy._processingChangedEventTestTask.JoinAsync();
// Assert
Assert.True(called);
}
[Fact]
public void Changed_NoopsIfProxyDisposed()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1);
var proxy = new DefaultProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
Dispatcher,
projectSnapshotManager,
JoinableTaskFactory);
var changedArgs = new ProjectChangeEventArgs(_projectSnapshot1, _projectSnapshot1, ProjectChangeKind.ProjectChanged);
proxy.Changed += (sender, args) => throw new InvalidOperationException("Should not have been called.");
proxy.Dispose();
// Act
projectSnapshotManager.TriggerChanged(changedArgs);
// Assert
Assert.Null(proxy._processingChangedEventTestTask);
}
[Fact]
public async Task GetLatestProjectsAsync_ReturnsSnapshotManagerProjects()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1);
using var proxy = new DefaultProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
Dispatcher,
projectSnapshotManager,
JoinableTaskFactory);
// Act
var projects = await proxy.GetLatestProjectsAsync();
// Assert
var project = Assert.Single(projects);
Assert.Same(_projectSnapshot1, project);
}
[Fact]
public async Task GetStateAsync_ReturnsProjectState()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1, _projectSnapshot2);
using var proxy = new DefaultProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
Dispatcher,
projectSnapshotManager,
JoinableTaskFactory);
// Act
var state = await JoinableTaskFactory.RunAsync(() => proxy.GetProjectManagerStateAsync(DisposalToken));
// Assert
var project1TagHelpers = await _projectSnapshot1.GetTagHelpersAsync(CancellationToken.None);
var project2TagHelpers = await _projectSnapshot2.GetTagHelpersAsync(CancellationToken.None);
Assert.Collection(
state.ProjectHandles,
handle =>
{
Assert.Equal("vsls:/path/to/project1.csproj", handle.FilePath.ToString());
Assert.Equal<TagHelperDescriptor>(project1TagHelpers, handle.ProjectWorkspaceState.TagHelpers);
},
handle =>
{
Assert.Equal("vsls:/path/to/project2.csproj", handle.FilePath.ToString());
Assert.Equal<TagHelperDescriptor>(project2TagHelpers, handle.ProjectWorkspaceState.TagHelpers);
});
}
[Fact]
public async Task GetStateAsync_CachesState()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1);
using var proxy = new DefaultProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
Dispatcher,
projectSnapshotManager,
JoinableTaskFactory);
// Act
var state1 = await JoinableTaskFactory.RunAsync(() => proxy.GetProjectManagerStateAsync(DisposalToken));
var state2 = await JoinableTaskFactory.RunAsync(() => proxy.GetProjectManagerStateAsync(DisposalToken));
// Assert
Assert.Same(state1, state2);
}
private class TestProjectSnapshotManager(params IProjectSnapshot[] projects) : IProjectSnapshotManager
{
private ImmutableArray<IProjectSnapshot> _projects = projects.ToImmutableArray();
public ImmutableArray<IProjectSnapshot> GetProjects() => _projects;
public event EventHandler<ProjectChangeEventArgs> Changed;
public void TriggerChanged(ProjectChangeEventArgs args)
{
Changed?.Invoke(this, args);
}
public IProjectSnapshot GetLoadedProject(ProjectKey projectKey)
=> throw new NotImplementedException();
public ImmutableArray<ProjectKey> GetAllProjectKeys(string projectFileName)
=> throw new NotImplementedException();
public bool IsDocumentOpen(string documentFilePath)
=> throw new NotImplementedException();
public bool TryGetLoadedProject(ProjectKey projectKey, [NotNullWhen(true)] out IProjectSnapshot project)
=> throw new NotImplementedException();
}
}

Просмотреть файл

@ -0,0 +1,231 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.ProjectEngineHost;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.LiveShare.Razor.Test;
using Microsoft.VisualStudio.Threading;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.VisualStudio.LiveShare.Razor.Host;
public class ProjectSnapshotManagerProxyTest : ProjectSnapshotManagerDispatcherTestBase
{
private readonly IProjectSnapshot _projectSnapshot1;
private readonly IProjectSnapshot _projectSnapshot2;
public ProjectSnapshotManagerProxyTest(ITestOutputHelper testOutput)
: base(testOutput)
{
var projectEngineFactoryProvider = StrictMock.Of<IProjectEngineFactoryProvider>();
var projectWorkspaceState1 = ProjectWorkspaceState.Create(ImmutableArray.Create(
TagHelperDescriptorBuilder.Create("test1", "TestAssembly1").Build()));
_projectSnapshot1 = new ProjectSnapshot(
ProjectState.Create(
projectEngineFactoryProvider,
new HostProject("/host/path/to/project1.csproj", "/host/path/to/obj", RazorConfiguration.Default, "project1"),
projectWorkspaceState1));
var projectWorkspaceState2 = ProjectWorkspaceState.Create(ImmutableArray.Create(
TagHelperDescriptorBuilder.Create("test2", "TestAssembly2").Build()));
_projectSnapshot2 = new ProjectSnapshot(
ProjectState.Create(
projectEngineFactoryProvider,
new HostProject("/host/path/to/project2.csproj", "/host/path/to/obj", RazorConfiguration.Default, "project2"),
projectWorkspaceState2));
}
[Fact]
public async Task CalculateUpdatedStateAsync_ReturnsStateForAllProjects()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1, _projectSnapshot2);
using var proxy = new ProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
projectSnapshotManager,
Dispatcher,
JoinableTaskFactory);
// Act
var state = await JoinableTaskFactory.RunAsync(() => proxy.CalculateUpdatedStateAsync(projectSnapshotManager.GetProjects()));
// Assert
var project1TagHelpers = await _projectSnapshot1.GetTagHelpersAsync(CancellationToken.None);
var project2TagHelpers = await _projectSnapshot2.GetTagHelpersAsync(CancellationToken.None);
Assert.Collection(
state.ProjectHandles,
handle =>
{
Assert.Equal("vsls:/path/to/project1.csproj", handle.FilePath.ToString());
Assert.Equal<TagHelperDescriptor>(project1TagHelpers, handle.ProjectWorkspaceState.TagHelpers);
},
handle =>
{
Assert.Equal("vsls:/path/to/project2.csproj", handle.FilePath.ToString());
Assert.Equal<TagHelperDescriptor>(project2TagHelpers, handle.ProjectWorkspaceState.TagHelpers);
});
}
[Fact]
public async Task Changed_TriggersOnSnapshotManagerChanged()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1);
using var proxy = new ProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
projectSnapshotManager,
Dispatcher,
JoinableTaskFactory);
var proxyAccessor = proxy.GetTestAccessor();
var changedArgs = new ProjectChangeEventArgs(_projectSnapshot1, _projectSnapshot1, ProjectChangeKind.ProjectChanged);
var called = false;
proxy.Changed += (sender, args) =>
{
called = true;
Assert.Equal($"vsls:/path/to/project1.csproj", args.ProjectFilePath.ToString());
Assert.Equal(ProjectProxyChangeKind.ProjectChanged, args.Kind);
Assert.NotNull(args.Newer);
Assert.Equal("vsls:/path/to/project1.csproj", args.Newer.FilePath.ToString());
};
// Act
projectSnapshotManager.TriggerChanged(changedArgs);
await proxyAccessor.ProcessingChangedEventTestTask.AssumeNotNull().JoinAsync();
// Assert
Assert.True(called);
}
[UIFact]
public void Changed_NoopsIfProxyDisposed()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1);
var proxy = new ProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
projectSnapshotManager,
Dispatcher,
JoinableTaskFactory);
var proxyAccessor = proxy.GetTestAccessor();
var changedArgs = new ProjectChangeEventArgs(_projectSnapshot1, _projectSnapshot1, ProjectChangeKind.ProjectChanged);
proxy.Changed += (sender, args) => throw new InvalidOperationException("Should not have been called.");
proxy.Dispose();
// Act
projectSnapshotManager.TriggerChanged(changedArgs);
// Assert
Assert.Null(proxyAccessor.ProcessingChangedEventTestTask);
}
[Fact]
public async Task GetLatestProjectsAsync_ReturnsSnapshotManagerProjects()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1);
using var proxy = new ProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
projectSnapshotManager,
Dispatcher,
JoinableTaskFactory);
// Act
var projects = await proxy.GetLatestProjectsAsync();
// Assert
var project = Assert.Single(projects);
Assert.Same(_projectSnapshot1, project);
}
[Fact]
public async Task GetStateAsync_ReturnsProjectState()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1, _projectSnapshot2);
using var proxy = new ProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
projectSnapshotManager,
Dispatcher,
JoinableTaskFactory);
// Act
var state = await JoinableTaskFactory.RunAsync(() => proxy.GetProjectManagerStateAsync(DisposalToken));
// Assert
var project1TagHelpers = await _projectSnapshot1.GetTagHelpersAsync(CancellationToken.None);
var project2TagHelpers = await _projectSnapshot2.GetTagHelpersAsync(CancellationToken.None);
Assert.Collection(
state.ProjectHandles,
handle =>
{
Assert.Equal("vsls:/path/to/project1.csproj", handle.FilePath.ToString());
Assert.Equal<TagHelperDescriptor>(project1TagHelpers, handle.ProjectWorkspaceState.TagHelpers);
},
handle =>
{
Assert.Equal("vsls:/path/to/project2.csproj", handle.FilePath.ToString());
Assert.Equal<TagHelperDescriptor>(project2TagHelpers, handle.ProjectWorkspaceState.TagHelpers);
});
}
[Fact]
public async Task GetStateAsync_CachesState()
{
// Arrange
var projectSnapshotManager = new TestProjectSnapshotManager(_projectSnapshot1);
using var proxy = new ProjectSnapshotManagerProxy(
new TestCollaborationSession(true),
projectSnapshotManager,
Dispatcher,
JoinableTaskFactory);
// Act
var state1 = await JoinableTaskFactory.RunAsync(() => proxy.GetProjectManagerStateAsync(DisposalToken));
var state2 = await JoinableTaskFactory.RunAsync(() => proxy.GetProjectManagerStateAsync(DisposalToken));
// Assert
Assert.Same(state1, state2);
}
private sealed class TestProjectSnapshotManager(params IProjectSnapshot[] projects) : IProjectSnapshotManager
{
private readonly ImmutableArray<IProjectSnapshot> _projects = projects.ToImmutableArray();
public ImmutableArray<IProjectSnapshot> GetProjects() => _projects;
public event EventHandler<ProjectChangeEventArgs>? Changed;
public void TriggerChanged(ProjectChangeEventArgs args)
{
Changed?.Invoke(this, args);
}
public IProjectSnapshot GetLoadedProject(ProjectKey projectKey)
=> throw new NotImplementedException();
public ImmutableArray<ProjectKey> GetAllProjectKeys(string projectFileName)
=> throw new NotImplementedException();
public bool IsDocumentOpen(string documentFilePath)
=> throw new NotImplementedException();
public bool TryGetLoadedProject(ProjectKey projectKey, [NotNullWhen(true)] out IProjectSnapshot project)
=> throw new NotImplementedException();
}
}