diff --git a/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.Linux.verified.txt b/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.Linux.verified.txt new file mode 100644 index 00000000000..7e88eaa0b95 --- /dev/null +++ b/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.Linux.verified.txt @@ -0,0 +1,12 @@ +]9;4;3;\ project failed with 1 error(s) and 1 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): error AA0000: Error! +[?25l +[?25h +Build summary: + project failed with 1 error(s) and 1 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): error AA0000: Error! + +Build failed with 1 error(s) and 1 warning(s) in 5.0s +]9;4;0;\ \ No newline at end of file diff --git a/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.OSX.verified.txt b/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.OSX.verified.txt new file mode 100644 index 00000000000..42a61e2fbbb --- /dev/null +++ b/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.OSX.verified.txt @@ -0,0 +1,12 @@ + project failed with 1 error(s) and 1 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): error AA0000: Error! +[?25l +[?25h +Build summary: + project failed with 1 error(s) and 1 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): error AA0000: Error! + +Build failed with 1 error(s) and 1 warning(s) in 5.0s + diff --git a/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.Windows.verified.txt b/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.Windows.verified.txt new file mode 100644 index 00000000000..7e88eaa0b95 --- /dev/null +++ b/src/MSBuild.UnitTests/Snapshots/TerminalLogger_Tests.PrintDetailedBuildSummary_FailedWithErrorAndWarning.Windows.verified.txt @@ -0,0 +1,12 @@ +]9;4;3;\ project failed with 1 error(s) and 1 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): error AA0000: Error! +[?25l +[?25h +Build summary: + project failed with 1 error(s) and 1 warning(s) (0.2s) + directory/file(1,2,3,4): warning AA0000: Warning! + directory/file(1,2,3,4): error AA0000: Error! + +Build failed with 1 error(s) and 1 warning(s) in 5.0s +]9;4;0;\ \ No newline at end of file diff --git a/src/MSBuild.UnitTests/TerminalLogger_Tests.cs b/src/MSBuild.UnitTests/TerminalLogger_Tests.cs index bfcf0ddf585..a97f2c683cc 100644 --- a/src/MSBuild.UnitTests/TerminalLogger_Tests.cs +++ b/src/MSBuild.UnitTests/TerminalLogger_Tests.cs @@ -414,6 +414,26 @@ public Task PrintBuildSummary_FailedWithErrors() return Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform(); } + [Fact] + public Task PrintDetailedBuildSummary_FailedWithErrorAndWarning() + { + string? originalParameters = _terminallogger.Parameters; + _terminallogger.Parameters = "SUMMARY"; + _terminallogger.ParseParameters(); + + InvokeLoggerCallbacksForSimpleProject(succeeded: false, () => + { + WarningRaised?.Invoke(_eventSender, MakeWarningEventArgs("Warning!")); + ErrorRaised?.Invoke(_eventSender, MakeErrorEventArgs("Error!")); + }); + + // Restore original parameters + _terminallogger.Parameters = originalParameters; + _terminallogger.ParseParameters(); + + return Verify(_outputWriter.ToString(), _settings).UniqueForOSPlatform(); + } + [Fact] public Task PrintBuildSummary_FailedWithErrorsAndWarnings() { diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index 0a0ac48dc13..ece0160d846 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1661,6 +1661,12 @@ 's' should reflect the localized abbreviation for seconds + + Build summary: + + A header used by Terminal Logger to introduce the build summary. + + failed with {0} error(s) diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index f3239abb2cb..ae1cc82bcde 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -65,6 +65,13 @@ akce proběhla úspěšně s {0} upozorněním(i). Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index d7fb58897c7..9a6f40c211a 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -65,6 +65,13 @@ erfolgreich mit {0} Warnung(en) Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index fc01707fc5d..a4efc5fbe67 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -65,6 +65,13 @@ correcto con {0} advertencias Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index 83094e546db..25303575605 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -65,6 +65,13 @@ a réussi avec {0} avertissement(s) Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 6c513308d99..1d6ba50eb0d 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -65,6 +65,13 @@ completato con {0} avvisi Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 6755b1fc171..d0ddddead98 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -65,6 +65,13 @@ {0} 件の警告付きで成功しました Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index 9e8b9de1dfa..8e6560daf49 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -65,6 +65,13 @@ {0} 경고와 함께 성공 Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 815886c7de9..a42e0e4f5e5 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -65,6 +65,13 @@ zakończono powodzeniem, z ostrzeżeniami w liczbie: {0} Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index d5738d0f8e9..be0a3cfcdf5 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -65,6 +65,13 @@ êxito(s) com {0} aviso(s) Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index eabd3b267b5..a6abaa1f4a0 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -65,6 +65,13 @@ успешно выполнено с предупреждениями ({0}) Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index eb4eb23bfba..5ba264c7cc6 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -65,6 +65,13 @@ {0} uyarıyla başarılı oldu Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 3f343348633..921a7a50e3f 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -65,6 +65,13 @@ 成功,出现 {0} 警告 Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index f14b8436e8b..e83d5bbd6d7 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -65,6 +65,13 @@ 成功但有 {0} 個警告 Part of Terminal Logger summary message: "Build {BuildResult_X} in {duration}s" + + + + Build summary: + Build summary: + + A header used by Terminal Logger to introduce the build summary. diff --git a/src/MSBuild/TerminalLogger/Project.cs b/src/MSBuild/TerminalLogger/Project.cs index eabfd989c3e..e32d3686dc7 100644 --- a/src/MSBuild/TerminalLogger/Project.cs +++ b/src/MSBuild/TerminalLogger/Project.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Microsoft.Build.Logging.TerminalLogger; @@ -12,12 +13,15 @@ namespace Microsoft.Build.Logging.TerminalLogger; /// internal sealed class Project { + private List? _buildMessages; + /// /// Initialized a new with the given . /// /// The target framework of the project or null if not multi-targeting. - public Project(string? targetFramework, StopwatchAbstraction? stopwatch) + public Project(string projectFile, string? targetFramework, StopwatchAbstraction? stopwatch) { + File = projectFile; TargetFramework = targetFramework; if (stopwatch is not null) @@ -31,6 +35,8 @@ public Project(string? targetFramework, StopwatchAbstraction? stopwatch) } } + public string File { get; } + /// /// A stopwatch to time the build of the project. /// @@ -56,17 +62,59 @@ public Project(string? targetFramework, StopwatchAbstraction? stopwatch) /// public bool IsCachePluginProject { get; set; } + /// + /// True if project built successfully; otherwise false. + /// + public bool Succeeded { get; set; } + + /// + /// The number of errors raised during the build of the project. + /// + public int ErrorCount { get; private set; } + + /// + /// The number of warnings raised during the build of the project. + /// + public int WarningCount { get; private set; } + + /// + /// True when the project has error or warning build messages; otherwise false. + /// + public bool HasErrorsOrWarnings => ErrorCount > 0 || WarningCount > 0; + /// /// A lazily initialized list of build messages/warnings/errors raised during the build. /// - public List? BuildMessages { get; private set; } + public IReadOnlyList? BuildMessages => _buildMessages; /// /// Adds a build message of the given severity to . /// public void AddBuildMessage(MessageSeverity severity, string message) { - BuildMessages ??= new List(); - BuildMessages.Add(new BuildMessage(severity, message)); + _buildMessages ??= new List(); + _buildMessages.Add(new BuildMessage(severity, message)); + + if (severity == MessageSeverity.Error) + { + ErrorCount++; + } + else if (severity == MessageSeverity.Warning) + { + WarningCount++; + } + } + + /// + /// Filters the build messages to only include errors and warnings. + /// + /// A sequence of error and warning build messages. + public IEnumerable GetBuildErrorAndWarningMessages() + { + return BuildMessages is null ? + Enumerable.Empty() : + BuildMessages.Where(message => + message.Severity == MessageSeverity.Error || + message.Severity == MessageSeverity.Warning); } } diff --git a/src/MSBuild/TerminalLogger/TerminalLogger.cs b/src/MSBuild/TerminalLogger/TerminalLogger.cs index 85a4baf1e08..d4dc4346cf9 100644 --- a/src/MSBuild/TerminalLogger/TerminalLogger.cs +++ b/src/MSBuild/TerminalLogger/TerminalLogger.cs @@ -221,6 +221,11 @@ public ProjectContext(BuildEventContext context) /// private bool _showCommandLine = false; + /// + /// Indicates whether to show the build summary. + /// + private bool? _showSummary; + private uint? _originalConsoleMode; /// @@ -320,6 +325,12 @@ private void ApplyParameter(string parameterName, string? parameterValue) case "SHOWCOMMANDLINE": TryApplyShowCommandLineParameter(parameterValue); break; + case "SUMMARY": + _showSummary = true; + break; + case "NOSUMMARY": + _showSummary = false; + break; } } @@ -334,9 +345,7 @@ private void ApplyVerbosityParameter(string? parameterValue) } else { - string errorCode; - string helpKeyword; - string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out errorCode, out helpKeyword, "InvalidVerbosity", parameterValue); + string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out string helpKeyword, "InvalidVerbosity", parameterValue); throw new LoggerException(message, null, errorCode, helpKeyword); } } @@ -401,15 +410,13 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e) _cts.Cancel(); _refresher?.Join(); - _projects.Clear(); - Terminal.BeginUpdate(); try { if (Verbosity > LoggerVerbosity.Quiet) { string duration = (e.Timestamp - _buildStartTime).TotalSeconds.ToString("F1"); - string buildResult = RenderBuildResult(e.Succeeded, _buildErrorsCount, _buildWarningsCount); + string buildResult = GetBuildResultString(e.Succeeded, _buildErrorsCount, _buildWarningsCount); Terminal.WriteLine(""); if (_testRunSummaries.Any()) @@ -437,6 +444,11 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e) Terminal.WriteLine(string.Join(CultureInfo.CurrentCulture.TextInfo.ListSeparator + " ", summaryAndTotalText, failedText, passedText, skippedText, durationText)); } + if (_showSummary == true) + { + RenderBuildSummary(); + } + if (_restoreFailed) { Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("RestoreCompleteWithMessage", @@ -461,6 +473,7 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e) Terminal.EndUpdate(); } + _projects.Clear(); _testRunSummaries.Clear(); _buildErrorsCount = 0; _buildWarningsCount = 0; @@ -469,6 +482,33 @@ private void BuildFinished(object sender, BuildFinishedEventArgs e) _testEndTime = null; } + private void RenderBuildSummary() + { + if (_buildErrorsCount == 0 && _buildWarningsCount == 0) + { + // No errors/warnings to display. + return; + } + + Terminal.WriteLine(ResourceUtilities.GetResourceString("BuildSummary")); + + foreach (Project project in _projects.Values.Where(p => p.HasErrorsOrWarnings)) + { + string duration = project.Stopwatch.ElapsedSeconds.ToString("F1"); + string buildResult = GetBuildResultString(project.Succeeded, project.ErrorCount, project.WarningCount); + string projectHeader = GetProjectFinishedHeader(project, buildResult, duration); + + Terminal.WriteLine(projectHeader); + + foreach (BuildMessage buildMessage in project.GetBuildErrorAndWarningMessages()) + { + Terminal.WriteLine($"{DoubleIndentation}{buildMessage.Message}"); + } + } + + Terminal.WriteLine(string.Empty); + } + private void StatusEventRaised(object sender, BuildStatusEventArgs e) { if (e is BuildCanceledEventArgs buildCanceledEventArgs) @@ -496,7 +536,7 @@ private void ProjectStarted(object sender, ProjectStartedEventArgs e) { targetFramework = null; } - _projects[c] = new(targetFramework, CreateStopwatch?.Invoke()); + _projects[c] = new(e.ProjectFile!, targetFramework, CreateStopwatch?.Invoke()); // First ever restore in the build is starting. if (e.TargetNames == "Restore" && !_restoreFinished) @@ -535,6 +575,8 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) if (_projects.TryGetValue(c, out Project? project)) { + project.Succeeded = e.Succeeded; + project.Stopwatch.Stop(); lock (_lock) { Terminal.BeginUpdate(); @@ -545,26 +587,16 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) string duration = project.Stopwatch.ElapsedSeconds.ToString("F1"); ReadOnlyMemory? outputPath = project.OutputPath; - string projectFile = e.ProjectFile is not null ? - Path.GetFileNameWithoutExtension(e.ProjectFile) : - string.Empty; - // Build result. One of 'failed', 'succeeded with warnings', or 'succeeded' depending on the build result and diagnostic messages // reported during build. - int countErrors = project.BuildMessages?.Count(m => m.Severity == MessageSeverity.Error) ?? 0; - int countWarnings = project.BuildMessages?.Count(m => m.Severity == MessageSeverity.Warning) ?? 0; - - string buildResult = RenderBuildResult(e.Succeeded, countErrors, countWarnings); - - bool haveErrors = countErrors > 0; - bool haveWarnings = countWarnings > 0; + string buildResult = GetBuildResultString(project.Succeeded, project.ErrorCount, project.WarningCount); // Check if we're done restoring. if (c == _restoreContext) { if (e.Succeeded) { - if (haveErrors || haveWarnings) + if (project.HasErrorsOrWarnings) { Terminal.WriteLine(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("RestoreCompleteWithMessage", buildResult, @@ -591,46 +623,8 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) else if (project.OutputPath is not null || project.BuildMessages is not null || project.IsTestProject) { // Show project build complete and its output - if (project.IsTestProject) - { - if (string.IsNullOrEmpty(project.TargetFramework)) - { - Terminal.Write(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestProjectFinished_NoTF", - Indentation, - projectFile, - buildResult, - duration)); - } - else - { - Terminal.Write(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TestProjectFinished_WithTF", - Indentation, - projectFile, - AnsiCodes.Colorize(project.TargetFramework, TargetFrameworkColor), - buildResult, - duration)); - } - } - else - { - if (string.IsNullOrEmpty(project.TargetFramework)) - { - Terminal.Write(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_NoTF", - Indentation, - projectFile, - buildResult, - duration)); - } - else - { - Terminal.Write(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("ProjectFinished_WithTF", - Indentation, - projectFile, - AnsiCodes.Colorize(project.TargetFramework, TargetFrameworkColor), - buildResult, - duration)); - } - } + string projectFinishedHeader = GetProjectFinishedHeader(project, buildResult, duration); + Terminal.Write(projectFinishedHeader); // Print the output path as a link if we have it. if (outputPath is not null) @@ -685,8 +679,8 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) } } - _buildErrorsCount += countErrors; - _buildWarningsCount += countWarnings; + _buildErrorsCount += project.ErrorCount; + _buildWarningsCount += project.WarningCount; DisplayNodes(); } @@ -698,6 +692,35 @@ private void ProjectFinished(object sender, ProjectFinishedEventArgs e) } } + private static string GetProjectFinishedHeader(Project project, string buildResult, string duration) + { + string projectFile = project.File is not null ? + Path.GetFileNameWithoutExtension(project.File) : + string.Empty; + + if (string.IsNullOrEmpty(project.TargetFramework)) + { + string resourceName = project.IsTestProject ? "TestProjectFinished_NoTF" : "ProjectFinished_NoTF"; + + return ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(resourceName, + Indentation, + projectFile, + buildResult, + duration); + } + else + { + string resourceName = project.IsTestProject ? "TestProjectFinished_WithTF" : "ProjectFinished_WithTF"; + + return ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(resourceName, + Indentation, + projectFile, + AnsiCodes.Colorize(project.TargetFramework, TargetFrameworkColor), + buildResult, + duration); + } + } + /// /// The callback. /// @@ -1051,12 +1074,12 @@ private void EraseNodes() #region Helpers /// - /// Print a build result summary to the output. + /// Construct a build result summary string. /// /// True if the build completed with success. /// True if the build has logged at least one error. /// True if the build has logged at least one warning. - private string RenderBuildResult(bool succeeded, int countErrors, int countWarnings) + private static string GetBuildResultString(bool succeeded, int countErrors, int countWarnings) { if (!succeeded) {