File: Testing\TestRunner.TestRunHandler.cs
Web Access
Project: src\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj (Microsoft.CodeAnalysis.LanguageServer)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Text;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.Handler.Testing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.LanguageServer.Testing;
 
internal partial class TestRunner
{
    private class TestRunHandler(BufferedProgress<RunTestsPartialResult> progress, TestProgress initialProgress, ILogger logger) : ITestRunEventsHandler
    {
        private readonly ILogger _logger = logger;
        private readonly BufferedProgress<RunTestsPartialResult> _progress = progress;
        private readonly TestProgress _initialProgress = initialProgress;
 
        private bool _isComplete = false;
 
        public void HandleLogMessage(TestMessageLevel level, string? message)
        {
            // Don't report log messages here.  The log output is dependent on the test framework being used and does not consistently
            // report the information we desire (for example the test names that passed).  Instead we report the test run information manually.
            // Any information here is also reported in the vs test console logs (written to the extension logs directory).
            return;
        }
 
        public void HandleRawMessage(string rawMessage)
        {
            // No need to do anything with raw messages.
            return;
        }
 
        public void HandleTestRunComplete(TestRunCompleteEventArgs testRunCompleteArgs, TestRunChangedEventArgs? lastChunkArgs, ICollection<AttachmentSet>? runContextAttachments, ICollection<string>? executorUris)
        {
            _isComplete = true;
            _logger.LogDebug($"Test run completed in {testRunCompleteArgs.ElapsedTimeInRunningTests}");
 
            // Report the last set of tests.
            var stats = CreateReport(testRunCompleteArgs.TestRunStatistics);
            var message = CreateTestCaseReportMessage(lastChunkArgs);
            var partialResult = new RunTestsPartialResult(LanguageServerResources.Running_tests, message ?? string.Empty, stats);
            _progress.Report(partialResult);
 
            // Report any errors running the tests
            if (testRunCompleteArgs.Error != null)
            {
                _progress.Report(partialResult with { Message = string.Format(LanguageServerResources.Test_run_error, testRunCompleteArgs.Error) });
                return;
            }
 
            var state = LanguageServerResources.Passed;
            if (testRunCompleteArgs.IsCanceled)
            {
                state = LanguageServerResources.Canceled;
            }
            else if (testRunCompleteArgs.IsAborted)
            {
                state = LanguageServerResources.Aborted;
            }
            else if (stats?.TestsFailed != 0)
            {
                state = LanguageServerResources.Failed;
            }
 
            // Report the test summary (similar to the dotnet test output).
            message = @$"==== {LanguageServerResources.Summary} ===={Environment.NewLine}{state}  - {string.Format(LanguageServerResources.Failed_0_Passed_1_Skipped_2_Total_3_Duration_4, stats?.TestsFailed, stats?.TestsPassed, stats?.TestsSkipped, stats?.TotalTests, RunTestsHandler.GetShortTimespan(testRunCompleteArgs.ElapsedTimeInRunningTests))}{Environment.NewLine}";
 
            _progress.Report(partialResult with { Message = message });
        }
 
        public void HandleTestRunStatsChange(TestRunChangedEventArgs? testRunChangedArgs)
        {
            Contract.ThrowIfTrue(_isComplete);
            if (testRunChangedArgs?.TestRunStatistics != null)
            {
                var stats = CreateReport(testRunChangedArgs.TestRunStatistics);
                var message = CreateTestCaseReportMessage(testRunChangedArgs);
                _progress.Report(new RunTestsPartialResult(LanguageServerResources.Running_tests, message, stats));
            }
        }
 
        public int LaunchProcessWithDebuggerAttached(TestProcessStartInfo testProcessStartInfo)
        {
            // TODO - implement debug tests.
            // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1799066/
            throw new NotImplementedException();
        }
 
        private TestProgress? CreateReport(ITestRunStatistics? testRunStatistics)
        {
            if (testRunStatistics?.Stats == null)
            {
                return null;
            }
 
            long passed = 0, failed = 0, skipped = 0;
            foreach (var (outcome, amount) in testRunStatistics.Stats)
            {
                switch (outcome)
                {
                    case TestOutcome.Passed:
                        passed = amount;
                        break;
                    case TestOutcome.Failed:
                        failed = amount;
                        break;
                    case TestOutcome.Skipped:
                        skipped = amount;
                        break;
                    default:
                        break;
                }
            }
 
            var stats = _initialProgress with { TestsPassed = passed, TestsFailed = failed, TestsSkipped = skipped };
            return stats;
        }
 
        /// <summary>
        /// Create a nicely formatted report of the test outcome including any error message or stack trace.
        /// Ensures that we're not creating duplicate blank lines and have proper indentation.
        /// </summary>
        private static string CreateTestCaseReportMessage(TestRunChangedEventArgs? testRunChangedEventArgs)
        {
            if (testRunChangedEventArgs?.NewTestResults == null)
            {
                return string.Empty;
            }
 
            var results = testRunChangedEventArgs.NewTestResults.Select(result =>
            {
                var messageBuilder = new StringBuilder();
                messageBuilder.AppendLine($"[{result.Outcome}] {result.TestCase.DisplayName}");
 
                if (!string.IsNullOrWhiteSpace(result.ErrorMessage))
                {
                    messageBuilder.AppendLine(IndentString($"{LanguageServerResources.Message}:", 4));
                    messageBuilder.AppendLine(IndentString(result.ErrorMessage, 8));
                }
 
                if (!string.IsNullOrWhiteSpace(result.ErrorStackTrace))
                {
                    messageBuilder.AppendLine(value: IndentString($"{LanguageServerResources.Stack_Trace}:", 4));
                    messageBuilder.AppendLine(IndentString(result.ErrorStackTrace, 8));
                }
 
                var standardOutputMessages = GetTestMessages(result.Messages, TestResultMessage.StandardOutCategory);
                if (standardOutputMessages.Length > 0)
                {
                    messageBuilder.AppendLine(value: IndentString($"{LanguageServerResources.Standard_Output_Messages}:", 4));
                    messageBuilder.AppendLine(FormatMessages(standardOutputMessages, 8));
                }
 
                var standardErrorMessages = GetTestMessages(result.Messages, TestResultMessage.StandardErrorCategory);
                if (standardErrorMessages.Length > 0)
                {
                    messageBuilder.AppendLine(value: IndentString($"{LanguageServerResources.Standard_Error_Messages}:", 4));
                    messageBuilder.AppendLine(FormatMessages(standardErrorMessages, 8));
                }
 
                var debugTraceMessages = GetTestMessages(result.Messages, TestResultMessage.DebugTraceCategory);
                if (debugTraceMessages.Length > 0)
                {
                    messageBuilder.AppendLine(value: IndentString($"{LanguageServerResources.Debug_Trace_Messages}:", 4));
                    messageBuilder.AppendLine(FormatMessages(debugTraceMessages, 8));
                }
 
                var additionalInfoMessages = GetTestMessages(result.Messages, TestResultMessage.AdditionalInfoCategory);
                if (additionalInfoMessages.Length > 0)
                {
                    messageBuilder.AppendLine(value: IndentString($"{LanguageServerResources.Additional_Info_Messages}:", 4));
                    messageBuilder.AppendLine(FormatMessages(additionalInfoMessages, 8));
                }
 
                return messageBuilder.ToString();
            });
 
            return string.Join("", results);
 
            static string IndentString(string text, int count)
            {
                var indentation = new string(' ', count);
                return text.Replace(Environment.NewLine, $"{Environment.NewLine}{indentation}").TrimEnd().Insert(0, indentation);
            }
 
            static ImmutableArray<TestResultMessage> GetTestMessages(Collection<TestResultMessage> messages, string requiredCategory)
            {
                return messages.WhereAsArray(static (msg, category) => msg.Category.Equals(category, StringComparison.OrdinalIgnoreCase), requiredCategory);
            }
 
            static string FormatMessages(ImmutableArray<TestResultMessage> messages, int indentation)
            {
                var builder = new StringBuilder();
                foreach (var message in messages)
                {
                    if (message.Text is null)
                        continue;
 
                    var indentedMessage = IndentString(message.Text, indentation);
                    if (!string.IsNullOrWhiteSpace(indentedMessage))
                        builder.Append(indentedMessage);
                }
 
                return builder.ToString();
            }
        }
    }
}