diff --git a/src/OmniSharp.Abstractions/Models/Events/EventTypes.cs b/src/OmniSharp.Abstractions/Models/Events/EventTypes.cs index 8d68a21401..38da098cbb 100644 --- a/src/OmniSharp.Abstractions/Models/Events/EventTypes.cs +++ b/src/OmniSharp.Abstractions/Models/Events/EventTypes.cs @@ -11,5 +11,7 @@ public static class EventTypes public const string PackageRestoreFinished = nameof(PackageRestoreFinished); public const string UnresolvedDependencies = nameof(UnresolvedDependencies); public const string ProjectConfiguration = nameof(ProjectConfiguration); + public const string ProjectDiagnosticStatus = nameof(ProjectDiagnosticStatus); + } } diff --git a/src/OmniSharp.Abstractions/Models/Events/ProjectAnalyzeStatusMessage.cs b/src/OmniSharp.Abstractions/Models/Events/ProjectAnalyzeStatusMessage.cs new file mode 100644 index 0000000000..bd84bf6cd9 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/Events/ProjectAnalyzeStatusMessage.cs @@ -0,0 +1,9 @@ +namespace OmniSharp.Models.Events +{ + public class ProjectDiagnosticStatusMessage + { + public ProjectDiagnosticStatus Status { get; set; } + public string ProjectFilePath { get; set; } + public string Type = "background"; + } +} \ No newline at end of file diff --git a/src/OmniSharp.Abstractions/Models/Events/ProjectDiagnosticStatus.cs b/src/OmniSharp.Abstractions/Models/Events/ProjectDiagnosticStatus.cs new file mode 100644 index 0000000000..e00bf04975 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/Events/ProjectDiagnosticStatus.cs @@ -0,0 +1,8 @@ +namespace OmniSharp.Models.Events +{ + public enum ProjectDiagnosticStatus + { + Started = 0, + Ready = 1 + } +} \ No newline at end of file diff --git a/src/OmniSharp.Abstractions/Models/v1/ReAnalyze/ReanalyzeRequest.cs b/src/OmniSharp.Abstractions/Models/v1/ReAnalyze/ReanalyzeRequest.cs new file mode 100644 index 0000000000..8cea94f7ad --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/ReAnalyze/ReanalyzeRequest.cs @@ -0,0 +1,10 @@ +using OmniSharp.Mef; +using OmniSharp.Models; + +namespace OmniSharp.Abstractions.Models.V1.ReAnalyze +{ + [OmniSharpEndpoint(OmniSharpEndpoints.ReAnalyze, typeof(ReAnalyzeRequest), typeof(ReanalyzeResponse))] + public class ReAnalyzeRequest: SimpleFileRequest + { + } +} \ No newline at end of file diff --git a/src/OmniSharp.Abstractions/Models/v1/ReAnalyze/ReanalyzeResponse.cs b/src/OmniSharp.Abstractions/Models/v1/ReAnalyze/ReanalyzeResponse.cs new file mode 100644 index 0000000000..02623e0080 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v1/ReAnalyze/ReanalyzeResponse.cs @@ -0,0 +1,9 @@ +using OmniSharp.Models; + +namespace OmniSharp.Abstractions.Models.V1.ReAnalyze +{ + public class ReanalyzeResponse : IAggregateResponse + { + public IAggregateResponse Merge(IAggregateResponse response) { return response; } + } +} \ No newline at end of file diff --git a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs index ca07b0ff17..18eb6e237b 100644 --- a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs +++ b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs @@ -43,6 +43,8 @@ public static class OmniSharpEndpoints public const string Close = "/close"; public const string Diagnostics = "/diagnostics"; + public const string ReAnalyze = "/reanalyze"; + public static class V2 { public const string GetCodeActions = "/v2/getcodeactions"; diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/DiagnosticsService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/DiagnosticsService.cs index 65b0f9666c..7fe6b24754 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/DiagnosticsService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/DiagnosticsService.cs @@ -29,7 +29,7 @@ public Task Handle(DiagnosticsRequest request) _forwarder.IsEnabled = true; } - _diagWorker.QueueAllDocumentsForDiagnostics(); + _diagWorker.QueueDocumentsForDiagnostics(); return Task.FromResult(new DiagnosticsResponse()); } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/ReAnalyzeService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/ReAnalyzeService.cs new file mode 100644 index 0000000000..1a957dd0e1 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/ReAnalyzeService.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; +using OmniSharp.Abstractions.Models.V1.ReAnalyze; +using OmniSharp.Mef; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; + +namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics +{ + [OmniSharpHandler(OmniSharpEndpoints.ReAnalyze, LanguageNames.CSharp)] + public class ReAnalyzeService : IRequestHandler + { + private readonly ICsDiagnosticWorker _diagWorker; + private readonly OmniSharpWorkspace _workspace; + private readonly ILogger _logger; + + [ImportingConstructor] + public ReAnalyzeService(ICsDiagnosticWorker diagWorker, OmniSharpWorkspace workspace, ILoggerFactory loggerFactory) + { + _diagWorker = diagWorker; + _workspace = workspace; + _logger = loggerFactory.CreateLogger(); + } + + public Task Handle(ReAnalyzeRequest request) + { + + if(!string.IsNullOrEmpty(request.FileName)) + { + var currentSolution = _workspace.CurrentSolution; + + var projectIds = WhenRequestIsProjectFileItselfGetFilesFromIt(request.FileName, currentSolution) + ?? GetProjectIdsFromDocumentFilePaths(request.FileName, currentSolution); + + _logger.LogInformation($"Queue analysis for project(s) {string.Join(", ", projectIds)}"); + + _diagWorker.QueueDocumentsForDiagnostics(projectIds); + } + else + { + _logger.LogInformation($"Queue analysis for all projects."); + _diagWorker.QueueDocumentsForDiagnostics(); + } + + return Task.FromResult(new ReanalyzeResponse()); + } + + private ImmutableArray? WhenRequestIsProjectFileItselfGetFilesFromIt(string FileName, Solution currentSolution) + { + var projects = currentSolution.Projects.Where(x => CompareProjectPath(FileName, x)).Select(x => x.Id).ToImmutableArray(); + + if(!projects.Any()) + return null; + + return projects; + } + + private static bool CompareProjectPath(string FileName, Project x) + { + return String.Compare( + x.FilePath, + FileName, + StringComparison.InvariantCultureIgnoreCase) == 0; + } + + private static ImmutableArray GetProjectIdsFromDocumentFilePaths(string FileName, Solution currentSolution) + { + return currentSolution + .GetDocumentIdsWithFilePath(FileName) + .Select(docId => currentSolution.GetDocument(docId).Project.Id) + .Distinct() + .ToImmutableArray(); + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkQueue.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkQueue.cs index a27af877fe..0559544976 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkQueue.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkQueue.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -12,98 +10,117 @@ namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics { public class AnalyzerWorkQueue { - private readonly int _throttlingMs = 300; + private class Queue + { + public Queue(TimeSpan throttling) + { + Throttling = throttling; + } - private readonly ConcurrentDictionary _workQueue = - new ConcurrentDictionary(); + public ImmutableHashSet WorkWaitingToExecute { get; set; } = ImmutableHashSet.Empty; + public ImmutableHashSet WorkExecuting { get; set; } = ImmutableHashSet.Empty; + public DateTime LastThrottlingBegan { get; set; } = DateTime.UtcNow; + public TimeSpan Throttling { get; } + public CancellationTokenSource WorkPendingToken { get; set; } + } - private readonly ConcurrentDictionary _currentWork = - new ConcurrentDictionary(); + private readonly Dictionary _queues = null; + private readonly ILogger _logger; private readonly Func _utcNow; private readonly int _maximumDelayWhenWaitingForResults; - private readonly ILogger _logger; + private readonly object _queueLock = new object(); - public AnalyzerWorkQueue(ILoggerFactory loggerFactory, Func utcNow = null, int timeoutForPendingWorkMs = 15*1000) + public AnalyzerWorkQueue(ILoggerFactory loggerFactory, int timeoutForPendingWorkMs, Func utcNow = null) { - utcNow = utcNow ?? (() => DateTime.UtcNow); + _queues = new Dictionary + { + { AnalyzerWorkType.Foreground, new Queue(TimeSpan.FromMilliseconds(150)) }, + { AnalyzerWorkType.Background, new Queue(TimeSpan.FromMilliseconds(1500)) } + }; + _logger = loggerFactory.CreateLogger(); - _utcNow = utcNow; + _utcNow = utcNow ?? (() => DateTime.UtcNow); _maximumDelayWhenWaitingForResults = timeoutForPendingWorkMs; } - public void PutWork(DocumentId documentId) + public void PutWork(IReadOnlyCollection documentIds, AnalyzerWorkType workType) { - _workQueue.AddOrUpdate(documentId, - (modified: DateTime.UtcNow, new CancellationTokenSource()), - (_, oldValue) => (modified: DateTime.UtcNow, oldValue.workDoneSource)); + lock (_queueLock) + { + var queue = _queues[workType]; + + if (queue.WorkWaitingToExecute.IsEmpty) + queue.LastThrottlingBegan = _utcNow(); + + if (queue.WorkPendingToken == null) + queue.WorkPendingToken = new CancellationTokenSource(); + + queue.WorkWaitingToExecute = queue.WorkWaitingToExecute.Union(documentIds); + } } - public ImmutableArray TakeWork() + public IReadOnlyCollection TakeWork(AnalyzerWorkType workType) { - lock (_workQueue) + lock (_queueLock) { - var now = _utcNow(); - var currentWork = _workQueue - .Where(x => ThrottlingPeriodNotActive(x.Value.modified, now)) - .OrderByDescending(x => x.Value.modified) - .Take(50) - .ToImmutableArray(); - - foreach (var work in currentWork) - { - _workQueue.TryRemove(work.Key, out _); - _currentWork.TryAdd(work.Key, work.Value); - } - - return currentWork.Select(x => x.Key).ToImmutableArray(); + var queue = _queues[workType]; + + if (IsThrottlingActive(queue) || queue.WorkWaitingToExecute.IsEmpty) + return ImmutableHashSet.Empty; + + queue.WorkExecuting = queue.WorkWaitingToExecute; + queue.WorkWaitingToExecute = ImmutableHashSet.Empty; + return queue.WorkExecuting; } } - private bool ThrottlingPeriodNotActive(DateTime modified, DateTime now) + private bool IsThrottlingActive(Queue queue) { - return (now - modified).TotalMilliseconds >= _throttlingMs; + return (_utcNow() - queue.LastThrottlingBegan).TotalMilliseconds <= queue.Throttling.TotalMilliseconds; } - public void MarkWorkAsCompleteForDocumentId(DocumentId documentId) + public void WorkComplete(AnalyzerWorkType workType) { - if(_currentWork.TryGetValue(documentId, out var work)) + lock (_queueLock) { - work.workDoneSource.Cancel(); - _currentWork.TryRemove(documentId, out _); + if(_queues[workType].WorkExecuting.IsEmpty) + return; + + _queues[workType].WorkPendingToken?.Cancel(); + _queues[workType].WorkPendingToken = null; + _queues[workType].WorkExecuting = ImmutableHashSet.Empty; } } - // Omnisharp V2 api expects that it can request current information of diagnostics any time, + // Omnisharp V2 api expects that it can request current information of diagnostics any time (single file/current document), // however analysis is worker based and is eventually ready. This method is used to make api look // like it's syncronous even that actual analysis may take a while. - public async Task WaitForResultsAsync(ImmutableArray documentIds) + public Task WaitForegroundWorkComplete() { - var items = new List<(DateTime modified, CancellationTokenSource workDoneSource)>(); + var queue = _queues[AnalyzerWorkType.Foreground]; - foreach (var documentId in documentIds) + if (queue.WorkPendingToken == null || (queue.WorkPendingToken == null && queue.WorkWaitingToExecute.IsEmpty)) + return Task.CompletedTask; + + return Task.Delay(_maximumDelayWhenWaitingForResults, queue.WorkPendingToken.Token) + .ContinueWith(task => LogTimeouts(task)); + } + + public bool TryPromote(DocumentId id) + { + if (_queues[AnalyzerWorkType.Background].WorkWaitingToExecute.Contains(id) || _queues[AnalyzerWorkType.Background].WorkExecuting.Contains(id)) { - if (_currentWork.ContainsKey(documentId)) - { - items.Add(_currentWork[documentId]); - } - else if (_workQueue.ContainsKey(documentId)) - { - items.Add(_workQueue[documentId]); - } + PutWork(new[] { id }, AnalyzerWorkType.Foreground); + return true; } - await Task.WhenAll(items.Select(item => - Task.Delay(_maximumDelayWhenWaitingForResults, item.workDoneSource.Token) - .ContinueWith(task => LogTimeouts(task, documentIds)))); + return false; } - // This logs wait's for documentId diagnostics that continue without getting current version from analyzer. - // This happens on larger solutions during initial load or situations where analysis slows down remarkably. - private void LogTimeouts(Task task, IEnumerable documentIds) + private void LogTimeouts(Task task) { - if (!task.IsCanceled) _logger.LogDebug($"Timeout before work got ready for one of documents {string.Join(",", documentIds)}."); + if (!task.IsCanceled) _logger.LogWarning($"Timeout before work got ready for foreground analysis queue. This is assertion to prevent complete api hang in case of error."); } } - } diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkType.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkType.cs new file mode 100644 index 0000000000..deea63426a --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkType.cs @@ -0,0 +1,8 @@ +namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics +{ + public enum AnalyzerWorkType + { + Background, Foreground + } + +} diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs index 28f1e5db70..64699d996f 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs @@ -184,9 +184,16 @@ private static async Task> GetDiagnosticsForDocument( } } - public ImmutableArray QueueAllDocumentsForDiagnostics() + public ImmutableArray QueueDocumentsForDiagnostics() { - var documents = _workspace.CurrentSolution.Projects.SelectMany(x => x.Documents).ToImmutableArray(); + var documents = _workspace.CurrentSolution.Projects.SelectMany(x => x.Documents); + QueueForDiagnosis(documents.Select(x => x.FilePath).ToImmutableArray()); + return documents.Select(x => x.Id).ToImmutableArray(); + } + + public ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray projectIds) + { + var documents = projectIds.SelectMany(projectId => _workspace.CurrentSolution.GetProject(projectId).Documents); QueueForDiagnosis(documents.Select(x => x.FilePath).ToImmutableArray()); return documents.Select(x => x.Id).ToImmutableArray(); } diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs index a61c9103d7..60522c8b19 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging; using OmniSharp.Helpers; using OmniSharp.Models.Diagnostics; +using OmniSharp.Models.Events; using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; using OmniSharp.Services; @@ -27,6 +28,7 @@ public class CSharpDiagnosticWorkerWithAnalyzers : ICsDiagnosticWorker new ConcurrentDictionary diagnostics)>(); private readonly ImmutableArray _providers; private readonly DiagnosticEventForwarder _forwarder; + private readonly OmniSharpOptions _options; private readonly OmniSharpWorkspace _workspace; // This is workaround. @@ -39,13 +41,15 @@ public CSharpDiagnosticWorkerWithAnalyzers( OmniSharpWorkspace workspace, [ImportMany] IEnumerable providers, ILoggerFactory loggerFactory, - DiagnosticEventForwarder forwarder) + DiagnosticEventForwarder forwarder, + OmniSharpOptions options) { _logger = loggerFactory.CreateLogger(); _providers = providers.ToImmutableArray(); - _workQueue = new AnalyzerWorkQueue(loggerFactory); + _workQueue = new AnalyzerWorkQueue(loggerFactory, timeoutForPendingWorkMs: options.RoslynExtensionsOptions.DocumentAnalysisTimeoutMs * 3); _forwarder = forwarder; + _options = options; _workspace = workspace; _workspaceAnalyzerOptionsConstructor = Assembly @@ -56,7 +60,8 @@ public CSharpDiagnosticWorkerWithAnalyzers( _workspace.WorkspaceChanged += OnWorkspaceChanged; - Task.Factory.StartNew(Worker, TaskCreationOptions.LongRunning); + Task.Factory.StartNew(() => Worker(AnalyzerWorkType.Foreground), TaskCreationOptions.LongRunning); + Task.Factory.StartNew(() => Worker(AnalyzerWorkType.Background), TaskCreationOptions.LongRunning); } private Task InitializeWithWorkspaceDocumentsIfNotYetDone() @@ -73,18 +78,11 @@ private Task InitializeWithWorkspaceDocumentsIfNotYetDone() .ContinueWith(_ => Task.Delay(50)) .ContinueWith(_ => { - var documentIds = QueueAllDocumentsForDiagnostics(); + var documentIds = QueueDocumentsForDiagnostics(); _logger.LogInformation($"Solution initialized -> queue all documents for code analysis. Initial document count: {documentIds.Length}."); }); } - public ImmutableArray QueueForDiagnosis(ImmutableArray documentPaths) - { - var documentIds = GetDocumentIdsFromPaths(documentPaths); - QueueForAnalysis(documentIds); - return documentIds; - } - public async Task> GetDiagnostics(ImmutableArray documentPaths) { await InitializeWithWorkspaceDocumentsIfNotYetDone(); @@ -96,8 +94,11 @@ public ImmutableArray QueueForDiagnosis(ImmutableArray docum private async Task> GetDiagnosticsByDocumentIds(ImmutableArray documentIds) { - await _workQueue.WaitForResultsAsync(documentIds); - + if(documentIds.Length == 1) + { + _workQueue.TryPromote(documentIds.Single()); + await _workQueue.WaitForegroundWorkComplete(); + } return _currentDiagnosticResults .Where(x => documentIds.Any(docId => docId == x.Key)) .SelectMany(x => x.Value.diagnostics, (k, v) => ((k.Value.projectName, v))) @@ -111,7 +112,7 @@ private ImmutableArray GetDocumentIdsFromPaths(ImmutableArray (projectId: solution.GetDocument(documentId)?.Project?.Id, documentId)) .Where(x => x.projectId != null) .GroupBy(x => x.projectId, x => x.documentId) @@ -128,9 +129,17 @@ private async Task Worker() foreach (var projectGroup in currentWorkGroupedByProjects) { + var projectPath = solution.GetProject(projectGroup.Key).FilePath; + + EventIfBackgroundWork(workType, projectPath, ProjectDiagnosticStatus.Started); + await AnalyzeProject(solution, projectGroup); + + EventIfBackgroundWork(workType, projectPath, ProjectDiagnosticStatus.Ready); } + _workQueue.WorkComplete(workType); + await Task.Delay(50); } catch (Exception ex) @@ -140,12 +149,15 @@ private async Task Worker() } } - private void QueueForAnalysis(ImmutableArray documentIds) + private void EventIfBackgroundWork(AnalyzerWorkType workType, string projectPath, ProjectDiagnosticStatus status) { - foreach (var document in documentIds) - { - _workQueue.PutWork(document); - } + if (workType == AnalyzerWorkType.Background) + _forwarder.ProjectAnalyzedInBackground(projectPath, status); + } + + private void QueueForAnalysis(ImmutableArray documentIds, AnalyzerWorkType workType) + { + _workQueue.PutWork(documentIds, workType); } private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEvent) @@ -156,7 +168,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEv case WorkspaceChangeKind.DocumentAdded: case WorkspaceChangeKind.DocumentReloaded: case WorkspaceChangeKind.DocumentInfoChanged: - QueueForAnalysis(ImmutableArray.Create(changeEvent.DocumentId)); + QueueForAnalysis(ImmutableArray.Create(changeEvent.DocumentId), AnalyzerWorkType.Foreground); break; case WorkspaceChangeKind.DocumentRemoved: if(!_currentDiagnosticResults.TryRemove(changeEvent.DocumentId, out _)) @@ -169,7 +181,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEv case WorkspaceChangeKind.ProjectReloaded: _logger.LogDebug($"Project {changeEvent.ProjectId} updated, reanalyzing it's diagnostics."); var projectDocumentIds = _workspace.CurrentSolution.GetProject(changeEvent.ProjectId).Documents.Select(x => x.Id).ToImmutableArray(); - QueueForAnalysis(projectDocumentIds); + QueueForAnalysis(projectDocumentIds, AnalyzerWorkType.Background); break; } } @@ -208,7 +220,7 @@ private async Task AnalyzeDocument(Project project, ImmutableArray diagnosticsWithAnalyzers) { _currentDiagnosticResults[document.Id] = (project.Name, diagnosticsWithAnalyzers); - _workQueue.MarkWorkAsCompleteForDocumentId(document.Id); EmitDiagnostics(_currentDiagnosticResults[document.Id].diagnostics); } @@ -272,10 +282,10 @@ private void EmitDiagnostics(ImmutableArray results) } } - public ImmutableArray QueueAllDocumentsForDiagnostics() + public ImmutableArray QueueDocumentsForDiagnostics() { var documentIds = _workspace.CurrentSolution.Projects.SelectMany(x => x.DocumentIds).ToImmutableArray(); - QueueForAnalysis(documentIds); + QueueForAnalysis(documentIds, AnalyzerWorkType.Background); return documentIds; } @@ -285,5 +295,12 @@ public ImmutableArray QueueAllDocumentsForDiagnostics() var allDocumentsIds = _workspace.CurrentSolution.Projects.SelectMany(x => x.DocumentIds).ToImmutableArray(); return await GetDiagnosticsByDocumentIds(allDocumentsIds); } + + public ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray projectIds) + { + var documentIds = projectIds.SelectMany(projectId => _workspace.CurrentSolution.GetProject(projectId).Documents.Select(x => x.Id)).ToImmutableArray(); + QueueForAnalysis(documentIds, AnalyzerWorkType.Background); + return documentIds; + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs index 9202e653f8..8458c09748 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs @@ -38,7 +38,7 @@ public CsharpDiagnosticWorkerComposer( { if(options.RoslynExtensionsOptions.EnableAnalyzersSupport) { - _implementation = new CSharpDiagnosticWorkerWithAnalyzers(workspace, providers, loggerFactory, forwarder); + _implementation = new CSharpDiagnosticWorkerWithAnalyzers(workspace, providers, loggerFactory, forwarder, options); } else { @@ -58,14 +58,14 @@ public CsharpDiagnosticWorkerComposer( return _implementation.GetDiagnostics(documentPaths); } - public ImmutableArray QueueAllDocumentsForDiagnostics() + public ImmutableArray QueueDocumentsForDiagnostics() { - return _implementation.QueueAllDocumentsForDiagnostics(); + return _implementation.QueueDocumentsForDiagnostics(); } - public ImmutableArray QueueForDiagnosis(ImmutableArray documentPaths) + public ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray projectIds) { - return _implementation.QueueForDiagnosis(documentPaths); + return _implementation.QueueDocumentsForDiagnostics(projectIds); } } } \ No newline at end of file diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs index 3295efa68b..1d22d70304 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs @@ -8,7 +8,7 @@ public interface ICsDiagnosticWorker { Task> GetDiagnostics(ImmutableArray documentPaths); Task> GetAllDiagnosticsAsync(); - ImmutableArray QueueForDiagnosis(ImmutableArray documentsPaths); - ImmutableArray QueueAllDocumentsForDiagnostics(); + ImmutableArray QueueDocumentsForDiagnostics(); + ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray projectId); } } \ No newline at end of file diff --git a/src/OmniSharp.Roslyn/DiagnosticEventForwarder.cs b/src/OmniSharp.Roslyn/DiagnosticEventForwarder.cs index 9c5b45ec61..f4f3f090cd 100644 --- a/src/OmniSharp.Roslyn/DiagnosticEventForwarder.cs +++ b/src/OmniSharp.Roslyn/DiagnosticEventForwarder.cs @@ -22,5 +22,10 @@ public void Forward(DiagnosticMessage message) { _emitter.Emit(EventTypes.Diagnostic, message); } + + public void ProjectAnalyzedInBackground(string projectFileName, ProjectDiagnosticStatus status) + { + _emitter.Emit(EventTypes.ProjectDiagnosticStatus, new ProjectDiagnosticStatusMessage { ProjectFilePath = projectFileName, Status = status }); + } } } diff --git a/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs b/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs index 879e22b4d8..ac875a47a8 100644 --- a/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs +++ b/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs @@ -8,6 +8,7 @@ namespace OmniSharp.Options public class RoslynExtensionsOptions { public bool EnableAnalyzersSupport { get; set; } + public int DocumentAnalysisTimeoutMs { get; set; } = 10 * 1000; public string[] LocationPaths { get; set; } @@ -27,7 +28,7 @@ public IEnumerable GetNormalizedLocationPaths(IOmniSharpEnvironment env) normalizePaths.Add(Path.Combine(env.TargetDirectory, locationPath)); } } - + return normalizePaths; } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/AnalyzerWorkerQueueFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/AnalyzerWorkerQueueFacts.cs index ce69ed174c..9df8d07f22 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/AnalyzerWorkerQueueFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/AnalyzerWorkerQueueFacts.cs @@ -47,94 +47,100 @@ public void Dispose() } } - [Fact] - public void WhenItemsAreAddedButThrotlingIsntOverNoWorkShouldBeReturned() + [Theory] + [InlineData(AnalyzerWorkType.Background)] + [InlineData(AnalyzerWorkType.Foreground)] + public void WhenItemsAreAddedButThrotlingIsntOverNoWorkShouldBeReturned(AnalyzerWorkType workType) { var now = DateTime.UtcNow; - var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now); + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 10*1000); var document = CreateTestDocumentId(); - queue.PutWork(document); - Assert.Empty(queue.TakeWork()); + queue.PutWork(new[] { document }, workType); + Assert.Empty(queue.TakeWork(workType)); } - [Fact] - public void WhenWorksIsAddedToQueueThenTheyWillBeReturned() + [Theory] + [InlineData(AnalyzerWorkType.Background)] + [InlineData(AnalyzerWorkType.Foreground)] + public void WhenWorksIsAddedToQueueThenTheyWillBeReturned(AnalyzerWorkType workType) { var now = DateTime.UtcNow; - var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now); + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 10*1000); var document = CreateTestDocumentId(); - queue.PutWork(document); + queue.PutWork(new[] { document }, workType); now = PassOverThrotlingPeriod(now); - var work = queue.TakeWork(); + var work = queue.TakeWork(workType); Assert.Contains(document, work); - Assert.Empty(queue.TakeWork()); + Assert.Empty(queue.TakeWork(workType)); } - [Fact] - public void WhenSameItemIsAddedMultipleTimesInRowThenThrottleItemAsOne() + [Theory] + [InlineData(AnalyzerWorkType.Background)] + [InlineData(AnalyzerWorkType.Foreground)] + public void WhenSameItemIsAddedMultipleTimesInRowThenThrottleItemAsOne(AnalyzerWorkType workType) { var now = DateTime.UtcNow; - var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now); + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 10*1000); var document = CreateTestDocumentId(); - queue.PutWork(document); - queue.PutWork(document); - queue.PutWork(document); + queue.PutWork(new[] { document }, workType); + queue.PutWork(new[] { document }, workType); + queue.PutWork(new[] { document }, workType); - Assert.Empty(queue.TakeWork()); + Assert.Empty(queue.TakeWork(workType)); now = PassOverThrotlingPeriod(now); - Assert.Contains(document, queue.TakeWork()); - Assert.Empty(queue.TakeWork()); + Assert.Contains(document, queue.TakeWork(workType)); + Assert.Empty(queue.TakeWork(workType)); } private static DateTime PassOverThrotlingPeriod(DateTime now) => now.AddSeconds(30); [Fact] - public void WhenWorkIsAddedThenWaitNextIterationOfItReady() + public void WhenForegroundWorkIsAddedThenWaitNextIterationOfItReady() { var now = DateTime.UtcNow; var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 500); var document = CreateTestDocumentId(); - queue.PutWork(document); + queue.PutWork(new[] { document }, AnalyzerWorkType.Foreground); - var pendingTask = queue.WaitForResultsAsync(new [] { document }.ToImmutableArray()); + var pendingTask = queue.WaitForegroundWorkComplete(); pendingTask.Wait(TimeSpan.FromMilliseconds(50)); Assert.False(pendingTask.IsCompleted); now = PassOverThrotlingPeriod(now); - var work = queue.TakeWork(); - queue.MarkWorkAsCompleteForDocumentId(document); + var work = queue.TakeWork(AnalyzerWorkType.Foreground); + queue.WorkComplete(AnalyzerWorkType.Foreground); pendingTask.Wait(TimeSpan.FromMilliseconds(50)); Assert.True(pendingTask.IsCompleted); } [Fact] - public void WhenWorkIsUnderAnalysisOutFromQueueThenWaitUntilNextIterationOfItIsReady() + public void WhenForegroundWorkIsUnderAnalysisOutFromQueueThenWaitUntilNextIterationOfItIsReady() { var now = DateTime.UtcNow; var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 500); var document = CreateTestDocumentId(); - queue.PutWork(document); + queue.PutWork(new[] { document }, AnalyzerWorkType.Foreground); now = PassOverThrotlingPeriod(now); - var work = queue.TakeWork(); + var work = queue.TakeWork(AnalyzerWorkType.Foreground); - var pendingTask = queue.WaitForResultsAsync(work); + var pendingTask = queue.WaitForegroundWorkComplete(); pendingTask.Wait(TimeSpan.FromMilliseconds(50)); Assert.False(pendingTask.IsCompleted); - queue.MarkWorkAsCompleteForDocumentId(document); + queue.WorkComplete(AnalyzerWorkType.Foreground); pendingTask.Wait(TimeSpan.FromMilliseconds(50)); Assert.True(pendingTask.IsCompleted); } @@ -147,12 +153,12 @@ public void WhenWorkIsWaitedButTimeoutForWaitIsExceededAllowContinue() var queue = new AnalyzerWorkQueue(loggerFactory, utcNow: () => now, timeoutForPendingWorkMs: 20); var document = CreateTestDocumentId(); - queue.PutWork(document); + queue.PutWork(new[] { document }, AnalyzerWorkType.Foreground); now = PassOverThrotlingPeriod(now); - var work = queue.TakeWork(); + var work = queue.TakeWork(AnalyzerWorkType.Foreground); - var pendingTask = queue.WaitForResultsAsync(work); + var pendingTask = queue.WaitForegroundWorkComplete(); var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(5)); pendingTask.Wait(cts.Token); @@ -171,59 +177,120 @@ public async Task WhenMultipleThreadsAreConsumingAnalyzerWorkerQueueItWorksAsExp var parallelQueues = Enumerable.Range(0, 10) .Select(_ => - Task.Run(() => { + Task.Run(() => + { var document = CreateTestDocumentId(); - queue.PutWork(document); + queue.PutWork(new[] { document }, AnalyzerWorkType.Foreground); now = PassOverThrotlingPeriod(now); - var work = queue.TakeWork(); - var pendingTask = queue.WaitForResultsAsync(work); + var work = queue.TakeWork(AnalyzerWorkType.Foreground); - foreach (var workDoc in work) - { - queue.MarkWorkAsCompleteForDocumentId(workDoc); - } + var pendingTask = queue.WaitForegroundWorkComplete(); + + queue.WaitForegroundWorkComplete(); pendingTask.Wait(TimeSpan.FromMilliseconds(300)); - })) + })) .ToArray(); await Task.WhenAll(parallelQueues); - Assert.Empty(queue.TakeWork()); + Assert.Empty(queue.TakeWork(AnalyzerWorkType.Foreground)); } [Fact] public async Task WhenWorkIsAddedAgainWhenPreviousIsAnalysing_ThenDontWaitAnotherOneToGetReady() { var now = DateTime.UtcNow; - var loggerFactory = new LoggerFactory(); - var queue = new AnalyzerWorkQueue(loggerFactory, utcNow: () => now); + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 10*1000); var document = CreateTestDocumentId(); - queue.PutWork(document); + queue.PutWork(new[] { document }, AnalyzerWorkType.Foreground); now = PassOverThrotlingPeriod(now); - var work = queue.TakeWork(); - var waitingCall = Task.Run(async () => await queue.WaitForResultsAsync(work)); + var work = queue.TakeWork(AnalyzerWorkType.Foreground); + var waitingCall = Task.Run(async () => await queue.WaitForegroundWorkComplete()); await Task.Delay(50); // User updates code -> document is queued again during period when theres already api call waiting // to continue. - queue.PutWork(document); + queue.PutWork(new[] { document }, AnalyzerWorkType.Foreground); // First iteration of work is done. - queue.MarkWorkAsCompleteForDocumentId(document); + queue.WorkComplete(AnalyzerWorkType.Foreground); // Waiting call continues because it's iteration of work is done, even when theres next // already waiting. await waitingCall; Assert.True(waitingCall.IsCompleted); - Assert.Empty(loggerFactory.Logger.RecordedMessages); + } + + [Fact] + public void WhenBackgroundWorkIsAdded_DontWaitIt() + { + var queue = new AnalyzerWorkQueue(new LoggerFactory(), timeoutForPendingWorkMs: 10*1000); + var document = CreateTestDocumentId(); + + queue.PutWork(new[] { document }, AnalyzerWorkType.Background); + + Assert.True(queue.WaitForegroundWorkComplete().IsCompleted); + } + + [Fact] + public void WhenSingleFileIsPromoted_ThenPromoteItFromBackgroundQueueToForeground() + { + var now = DateTime.UtcNow; + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 10*1000); + var document = CreateTestDocumentId(); + + queue.PutWork(new[] { document }, AnalyzerWorkType.Background); + + queue.TryPromote(document); + + now = PassOverThrotlingPeriod(now); + + Assert.NotEmpty(queue.TakeWork(AnalyzerWorkType.Foreground)); + } + + [Fact] + public void WhenFileIsntAtBackgroundQueueAndTriedToBePromoted_ThenDontDoNothing() + { + var now = DateTime.UtcNow; + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 10*1000); + var document = CreateTestDocumentId(); + + queue.TryPromote(document); + + now = PassOverThrotlingPeriod(now); + + Assert.Empty(queue.TakeWork(AnalyzerWorkType.Foreground)); + } + + [Fact] + public void WhenFileIsProcessingInBackgroundQueue_ThenPromoteItAsForeground() + { + var now = DateTime.UtcNow; + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 10*1000); + var document = CreateTestDocumentId(); + + queue.PutWork(new[] { document }, AnalyzerWorkType.Background); + + now = PassOverThrotlingPeriod(now); + + var activeWork = queue.TakeWork(AnalyzerWorkType.Background); + + queue.TryPromote(document); + + now = PassOverThrotlingPeriod(now); + + var foregroundWork = queue.TakeWork(AnalyzerWorkType.Foreground); + + Assert.NotEmpty(foregroundWork); + Assert.NotEmpty(activeWork); } private DocumentId CreateTestDocumentId() diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs index 6c1d718978..95fa4744e6 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs @@ -102,16 +102,15 @@ public async Task When_custom_analyzers_are_executed_then_return_results() { using (var host = GetHost()) { - var testFile = new TestFile("testFile.cs", "class _this_is_invalid_test_class_name { int n = true; }"); + var testFile = new TestFile("testFile_66.cs", "class _this_is_invalid_test_class_name { int n = true; }"); - host.AddFilesToWorkspace(testFile); + var testAnalyzerRef = new TestAnalyzerReference("TS1100"); - var testAnalyzerRef = new TestAnalyzerReference("TS1234", isEnabledByDefault: true); + var projectIds = AddProjectWitFile(host, testFile, testAnalyzerRef); - AddProjectWitFile(host, testFile, testAnalyzerRef); + var result = await host.RequestCodeCheckAsync("testFile_66.cs"); - var result = await host.RequestCodeCheckAsync(); - Assert.Contains(result.QuickFixes, f => f.Text.Contains(testAnalyzerRef.Id.ToString())); + Assert.Contains(result.QuickFixes.OfType(), f => f.Text.Contains(testAnalyzerRef.Id.ToString())); } } @@ -177,6 +176,8 @@ public async Task When_rules_udpate_diagnostic_severity_then_show_them_with_new_ host.Workspace.UpdateDiagnosticOptionsForProject(projectId, testRules.ToImmutableDictionary()); var result = await host.RequestCodeCheckAsync("testFile_2.cs"); + + var bar = result.QuickFixes.ToList(); Assert.Contains(result.QuickFixes.OfType(), f => f.Text.Contains(testAnalyzerRef.Id.ToString()) && f.LogLevel == "Hidden"); } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsFacts.cs index 3e4bffba8f..2906aa7ff0 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsFacts.cs @@ -2,9 +2,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using OmniSharp.Models.CodeCheck; using OmniSharp.Models.Diagnostics; -using OmniSharp.Roslyn.CSharp.Services.Diagnostics; using TestUtility; using Xunit; using Xunit.Abstractions; @@ -41,17 +39,35 @@ private OmniSharpTestHost GetHost(bool roslynAnalyzersEnabled) return OmniSharpTestHost.Create(testOutput: _testOutput, configurationData: new Dictionary() { { "RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString() } }); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task CheckAllFiles(bool roslynAnalyzersEnabled) + [Fact] + public async Task CheckAllFilesOnNonAnalyzerReturnImmediatlyAllResults() { - using (var host = GetHost(roslynAnalyzersEnabled)) + using (var host = GetHost(roslynAnalyzersEnabled: false)) + { + host.AddFilesToWorkspace( + new TestFile("a.cs", "class C1 { int n = true; }"), + new TestFile("b.cs", "class C2 { int n = true; }")); + + var quickFixes = await host.RequestCodeCheckAsync(); + + Assert.Contains(quickFixes.QuickFixes, x => x.Text.Contains("CS0029") && x.FileName == "a.cs"); + Assert.Contains(quickFixes.QuickFixes, x => x.Text.Contains("CS0029") && x.FileName == "b.cs"); + } + } + + [Fact] + public async Task CheckAllFilesWithAnalyzersWillEventuallyReturnAllResults() + { + using (var host = GetHost(roslynAnalyzersEnabled: true)) { host.AddFilesToWorkspace( new TestFile("a.cs", "class C1 { int n = true; }"), new TestFile("b.cs", "class C2 { int n = true; }")); + await TestHelpers.WaitUntil(async () => ( + await host.RequestCodeCheckAsync()).QuickFixes.Any(x => x.FileName == "a.cs") && + (await host.RequestCodeCheckAsync()).QuickFixes.Any(x => x.FileName == "b.cs"), frequency: 100, timeout: 10000); + var quickFixes = await host.RequestCodeCheckAsync(); Assert.Contains(quickFixes.QuickFixes, x => x.Text.Contains("CS0029") && x.FileName == "a.cs"); diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.cs index 34168f7c6d..64e6ad6697 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.cs @@ -27,7 +27,7 @@ public async Task CodeCheckSpecifiedFileOnly(string filename) var testFile = new TestFile(filename, "class C { int n = true; }"); - var emitter = new DiagnosticTestEmitter(); + var emitter = new TestEventEmitter(); var forwarder = new DiagnosticEventForwarder(emitter) { IsEnabled = true @@ -44,7 +44,7 @@ public async Task CodeCheckSpecifiedFileOnly(string filename) private CSharpDiagnosticWorkerWithAnalyzers CreateDiagnosticService(DiagnosticEventForwarder forwarder) { - return new CSharpDiagnosticWorkerWithAnalyzers(SharedOmniSharpTestHost.Workspace, Enumerable.Empty(), this.LoggerFactory, forwarder); + return new CSharpDiagnosticWorkerWithAnalyzers(SharedOmniSharpTestHost.Workspace, Enumerable.Empty(), this.LoggerFactory, forwarder, new OmniSharpOptions()); } [Theory(Skip = "Test needs to be updated for service changes")] @@ -58,7 +58,7 @@ public async Task CheckAllFiles(string filename1, string filename2) var testFile2 = new TestFile(filename2, "class C2 { int n = true; }"); SharedOmniSharpTestHost.AddFilesToWorkspace(testFile1, testFile2); - var emitter = new DiagnosticTestEmitter(); + var emitter = new TestEventEmitter(); var forwarder = new DiagnosticEventForwarder(emitter); var service = CreateDiagnosticService(forwarder); @@ -82,7 +82,7 @@ public async Task EnablesWhenEndPointIsHit(string filename1, string filename2) var testFile2 = new TestFile(filename2, "class C2 { int n = true; }"); SharedOmniSharpTestHost.AddFilesToWorkspace(testFile1, testFile2); - var emitter = new DiagnosticTestEmitter(); + var emitter = new TestEventEmitter(); var forwarder = new DiagnosticEventForwarder(emitter); var service = CreateDiagnosticService(forwarder); diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/ReAnalysisFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/ReAnalysisFacts.cs new file mode 100644 index 0000000000..0be14e9218 --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/ReAnalysisFacts.cs @@ -0,0 +1,129 @@ +using System.Linq; +using System.Threading.Tasks; +using OmniSharp.Abstractions.Models.V1.ReAnalyze; +using OmniSharp.Models.ChangeBuffer; +using OmniSharp.Models.Events; +using OmniSharp.Roslyn.CSharp.Services.Buffer; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public class ReAnalysisFacts + { + private readonly ITestOutputHelper _testOutput; + private readonly TestEventEmitter _eventListener; + + public ReAnalysisFacts(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + _eventListener = new TestEventEmitter(); + } + + + [Fact] + public async Task WhenReAnalyzeIsExecutedForAll_ThenReanalyzeAllFiles() + { + using (var host = GetHost()) + { + var changeBufferHandler = host.GetRequestHandler(OmniSharpEndpoints.ChangeBuffer); + var reAnalyzeHandler = host.GetRequestHandler(OmniSharpEndpoints.ReAnalyze); + + host.AddFilesToWorkspace(new TestFile("a.cs", "public class A: B { }"), new TestFile("b.cs", "public class B { }")); + + await host.RequestCodeCheckAsync("a.cs"); + + var newContent = "ThisDoesntContainValidReferenceAsBClassAnyMore"; + + await changeBufferHandler.Handle(new ChangeBufferRequest() + { + StartLine = 0, + StartColumn = 0, + EndLine = 0, + EndColumn = newContent.Length, + NewText = newContent, + FileName = "b.cs" + }); + + await reAnalyzeHandler.Handle(new ReAnalyzeRequest()); + + var quickFixes = await host.RequestCodeCheckAsync("a.cs"); + + // Reference to B is lost, a.cs should contain error about invalid reference to it. + // error CS0246: The type or namespace name 'B' could not be found + Assert.Contains(quickFixes.QuickFixes.Select(x => x.ToString()), x => x.Contains("CS0246")); + } + } + + [Fact] + public async Task WhenReanalyzeIsExecuted_ThenSendEventWhenAnalysisOfProjectIsReady() + { + using (var host = GetHost()) + { + var reAnalyzeHandler = host.GetRequestHandler(OmniSharpEndpoints.ReAnalyze); + + var projectId = host.AddFilesToWorkspace(new TestFile("a.cs", "public class A { }")).First(); + var project = host.Workspace.CurrentSolution.GetProject(projectId); + + _eventListener.Clear(); + + await reAnalyzeHandler.Handle(new ReAnalyzeRequest()); + + await _eventListener.ExpectForEmitted(x => x.ProjectFilePath == project.FilePath && x.Status == ProjectDiagnosticStatus.Started); + await _eventListener.ExpectForEmitted(x => x.ProjectFilePath == project.FilePath && x.Status == ProjectDiagnosticStatus.Ready); + } + } + + [Fact] + public async Task WhenReanalyzeIsExecutedForFileInProject_ThenOnlyAnalyzeProject() + { + using (var host = GetHost()) + { + var reAnalyzeHandler = host.GetRequestHandler(OmniSharpEndpoints.ReAnalyze); + + var projectAId = host.AddFilesToWorkspace(new TestFile("a.cs", "public class A { }")).First(); + var projectA = host.Workspace.CurrentSolution.GetProject(projectAId); + + _eventListener.Clear(); + + await reAnalyzeHandler.Handle(new ReAnalyzeRequest + { + FileName = projectA.Documents.Single(x => x.FilePath.EndsWith("a.cs")).FilePath + }); + + await _eventListener.ExpectForEmitted(x => x.ProjectFilePath == projectA.FilePath && x.Status == ProjectDiagnosticStatus.Started); + await _eventListener.ExpectForEmitted(x => x.ProjectFilePath == projectA.FilePath && x.Status == ProjectDiagnosticStatus.Ready); + } + } + + [Fact] + public async Task WhenCurrentFileIsProjectItself_ThenReAnalyzeItAsExpected() + { + using (var host = GetHost()) + { + var reAnalyzeHandler = host.GetRequestHandler(OmniSharpEndpoints.ReAnalyze); + + var projectId = host.AddFilesToWorkspace(new TestFile("a.cs", "public class A { }")).First(); + var project = host.Workspace.CurrentSolution.GetProject(projectId); + + _eventListener.Clear(); + + await reAnalyzeHandler.Handle(new ReAnalyzeRequest + { + FileName = project.FilePath + }); + + await _eventListener.ExpectForEmitted(x => x.ProjectFilePath == project.FilePath && x.Status == ProjectDiagnosticStatus.Started); + await _eventListener.ExpectForEmitted(x => x.ProjectFilePath == project.FilePath && x.Status == ProjectDiagnosticStatus.Ready); + } + } + + private OmniSharpTestHost GetHost() + { + return OmniSharpTestHost.Create(testOutput: _testOutput, + configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled: true), eventEmitter: _eventListener); + } + } +} \ No newline at end of file diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.DiagnosticTestEmitter.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/TestEventEmitter.cs similarity index 63% rename from tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.DiagnosticTestEmitter.cs rename to tests/OmniSharp.Roslyn.CSharp.Tests/TestEventEmitter.cs index 5136f0adbf..da0e41afc2 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.DiagnosticTestEmitter.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/TestEventEmitter.cs @@ -1,23 +1,18 @@ -using System; +using System; using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; using OmniSharp.Eventing; -using OmniSharp.Models.Diagnostics; namespace OmniSharp.Roslyn.CSharp.Tests { - public partial class DiagnosticsV2Facts - { - private class DiagnosticTestEmitter : IEventEmitter + public class TestEventEmitter : IEventEmitter { - public readonly ConcurrentBag Messages = new ConcurrentBag(); + public ImmutableArray Messages { get; private set; } = ImmutableArray.Empty; - private readonly TaskCompletionSource _tcs; - - public async Task ExpectForEmitted(Expression> predicate) + public async Task ExpectForEmitted(Expression> predicate) { var asCompiledPredicate = predicate.Compile(); @@ -32,18 +27,20 @@ public async Task ExpectForEmitted(Expression> pred await Task.Delay(250); } - throw new InvalidOperationException($"Timeout reached before expected event count reached before prediction {predicate} came true, current diagnostics '{String.Join(";", Messages.SelectMany(x => x.Results))}'"); + throw new InvalidOperationException($"Timeout reached before expected event count reached before prediction {predicate} came true, current diagnostics '{String.Join(";", Messages)}'"); } - public DiagnosticTestEmitter() + public void Clear() { - _tcs = new TaskCompletionSource(); + Messages = ImmutableArray.Empty; } public void Emit(string kind, object args) { - Messages.Add((DiagnosticMessage)args); + if(args is T asT) + { + Messages = Messages.Add(asT); + } } } - } -} +} \ No newline at end of file diff --git a/tests/TestUtility/OmniSharpTestHost.cs b/tests/TestUtility/OmniSharpTestHost.cs index f66ab824f2..4143d66b1f 100644 --- a/tests/TestUtility/OmniSharpTestHost.cs +++ b/tests/TestUtility/OmniSharpTestHost.cs @@ -13,6 +13,7 @@ using OmniSharp.Cake; using OmniSharp.DotNet; using OmniSharp.DotNetTest.Models; +using OmniSharp.Eventing; using OmniSharp.Mef; using OmniSharp.Models.WorkspaceInformation; using OmniSharp.MSBuild; @@ -102,11 +103,12 @@ public static OmniSharpTestHost Create( IEnumerable> configurationData = null, DotNetCliVersion dotNetCliVersion = DotNetCliVersion.Current, IEnumerable additionalExports = null, - [CallerMemberName] string callerName = "") + [CallerMemberName] string callerName = "", + IEventEmitter eventEmitter = null) { var environment = new OmniSharpEnvironment(path, logLevel: LogLevel.Trace); - var serviceProvider = TestServiceProvider.Create(testOutput, environment, configurationData, dotNetCliVersion); + var serviceProvider = TestServiceProvider.Create(testOutput, environment, configurationData, dotNetCliVersion, eventEmitter); return Create(serviceProvider, additionalExports, callerName); } @@ -127,9 +129,9 @@ public THandler GetRequestHandler(string name, string languageName = L return (THandler)_handlers[(name, languageName)].Value; } - public void AddFilesToWorkspace(params TestFile[] testFiles) + public IEnumerable AddFilesToWorkspace(params TestFile[] testFiles) { - TestHelpers.AddProjectToWorkspace( + var projects = TestHelpers.AddProjectToWorkspace( this.Workspace, "project.csproj", new[] { "net472" }, @@ -139,6 +141,8 @@ public void AddFilesToWorkspace(params TestFile[] testFiles) { TestHelpers.AddCsxProjectToWorkspace(Workspace, csxFile); } + + return projects; } public void ClearWorkspace() diff --git a/tests/TestUtility/TestHelpers.cs b/tests/TestUtility/TestHelpers.cs index 260197aca3..73f3b75b5d 100644 --- a/tests/TestUtility/TestHelpers.cs +++ b/tests/TestUtility/TestHelpers.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Scripting.Hosting; @@ -121,7 +122,7 @@ public static MSBuildInstance AddDotNetCoreToFakeInstance(this MSBuildInstance i public static Dictionary GetConfigurationDataWithAnalyzerConfig(bool roslynAnalyzersEnabled = false, Dictionary existingConfiguration = null) { - if(existingConfiguration == null) + if (existingConfiguration == null) { return new Dictionary() { { "RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString() } }; } @@ -130,7 +131,18 @@ public static Dictionary GetConfigurationDataWithAnalyzerConfig( copyOfExistingConfigs.Add("RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString()); return copyOfExistingConfigs; + } + + public static async Task WaitUntil(Func> condition, int frequency = 25, int timeout = -1) + { + var waitTask = Task.Run(async () => + { + while (!await condition()) await Task.Delay(frequency); + }); + if (waitTask != await Task.WhenAny(waitTask, + Task.Delay(timeout))) + throw new TimeoutException(); } } }