From 30fba81da9b6de299b0db2cc17284a4521f26fa2 Mon Sep 17 00:00:00 2001 From: Gary Ewan Park Date: Mon, 26 May 2025 11:00:18 +0100 Subject: [PATCH] (#4475) Always show task execution summary Currently, Cake prints out a task execution summary on each successful execution. This summary includes which tasks were executed or skipped, and the duration of the task. However, then an execution of Cake fails, the task summary is not printed, and it is necessary to scroll through the Cake output, to find out what task failed. You also lose the information about which tasks, if any, were skipped. Any changes to this workflow will have to be done carefully though, as we don't want to alter how tasks are executed, in what order, or what is done in the cases of ContinueOnError, etc. There are a number of unit tests that check for this execution, and these will need to continue to pass. This commit aims to address this problem by introducing a new CakeReportException, which has a property called Report of type CakeReport. That way, when an exception needs to be thrown, the current execution status report can be passed along with it, and from there, the task execution summary can be printed. The introduction of the CakeReportException has meant that a number of unit tests have had to be updated, since there is an expectation that an InvalidOperationException will be returned, however, I think that this is "ok", since it hasn't meant a change to how the workflow of task executions completes. --- src/Cake.Cli/Hosts/BuildScriptHost.cs | 22 ++++-- .../CakeSpectreReportPrinter.cs | 10 ++- src/Cake.Core.Tests/Unit/CakeEngineTests.cs | 36 ++++----- src/Cake.Core/CakeEngine.cs | 29 +++---- src/Cake.Core/CakeReportException.cs | 77 +++++++++++++++++++ src/Cake.Core/CakeReportPrinter.cs | 14 ++-- 6 files changed, 142 insertions(+), 46 deletions(-) create mode 100644 src/Cake.Core/CakeReportException.cs diff --git a/src/Cake.Cli/Hosts/BuildScriptHost.cs b/src/Cake.Cli/Hosts/BuildScriptHost.cs index 63a3c07ad1..7dd7ee1bc9 100644 --- a/src/Cake.Cli/Hosts/BuildScriptHost.cs +++ b/src/Cake.Cli/Hosts/BuildScriptHost.cs @@ -84,14 +84,26 @@ public override async Task RunTargetsAsync(IEnumerable targe private async Task internalRunTargetAsync() { - var report = await Engine.RunTargetAsync(_context, _executionStrategy, Settings).ConfigureAwait(false); - - if (report != null && !report.IsEmpty) + try { - _reportPrinter.Write(report); + var report = await Engine.RunTargetAsync(_context, _executionStrategy, Settings).ConfigureAwait(false); + + if (report != null && !report.IsEmpty) + { + _reportPrinter.Write(report); + } + + return report; } + catch (CakeReportException cre) + { + if (cre.Report != null && !cre.Report.IsEmpty) + { + _reportPrinter.Write(cre.Report); + } - return report; + throw; + } } } } \ No newline at end of file diff --git a/src/Cake.Cli/Infrastructure/CakeSpectreReportPrinter.cs b/src/Cake.Cli/Infrastructure/CakeSpectreReportPrinter.cs index 4e14ce2f05..f076484cb5 100644 --- a/src/Cake.Cli/Infrastructure/CakeSpectreReportPrinter.cs +++ b/src/Cake.Cli/Infrastructure/CakeSpectreReportPrinter.cs @@ -46,6 +46,10 @@ public void Write(CakeReport report) new Text("Duration", rowStyle)).Footer( new Text(FormatTime(GetTotalTime(report)), rowStyle))); + table.AddColumn( + new TableColumn( + new Text("Status", rowStyle))); + if (includeSkippedReasonColumn) { table.AddColumn(new TableColumn(new Text("Skip Reason", rowStyle))); @@ -59,12 +63,14 @@ public void Write(CakeReport report) { table.AddRow(new Markup(item.TaskName, itemStyle), new Markup(FormatDuration(item), itemStyle), + new Markup(item.ExecutionStatus.ToString(), itemStyle), new Markup(item.SkippedMessage, itemStyle)); } else { table.AddRow(new Markup(item.TaskName, itemStyle), - new Markup(FormatDuration(item), itemStyle)); + new Markup(FormatDuration(item), itemStyle), + new Markup(item.ExecutionStatus.ToString(), itemStyle)); } } @@ -122,7 +128,7 @@ private static string FormatDuration(CakeReportEntry item) { if (item.ExecutionStatus == CakeTaskExecutionStatus.Skipped) { - return "Skipped"; + return "-"; } return FormatTime(item.Duration); diff --git a/src/Cake.Core.Tests/Unit/CakeEngineTests.cs b/src/Cake.Core.Tests/Unit/CakeEngineTests.cs index 99cc3940fe..cb91a79372 100644 --- a/src/Cake.Core.Tests/Unit/CakeEngineTests.cs +++ b/src/Cake.Core.Tests/Unit/CakeEngineTests.cs @@ -216,7 +216,7 @@ public async Task Should_Throw_If_Target_Task_Is_Skipped() engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings)); // Then - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Could not reach target 'A' since it was skipped due to a criteria.", result?.Message); } @@ -442,7 +442,7 @@ public async Task Should_Not_Catch_Exceptions_From_Task_If_ContinueOnError_Is_No engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings)); // Then - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Whoopsie", result?.Message); } @@ -705,7 +705,7 @@ public async Task Should_Propagate_Exception_From_Error_Handler() engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings)); // Then - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Totally my fault", result?.Message); } @@ -743,7 +743,7 @@ public async Task Should_Throw_If_Target_Cannot_Be_Reached_Due_To_Constraint() engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings)); // Then - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Could not reach target 'B' since it was skipped due to a criteria.", result?.Message); } @@ -826,7 +826,7 @@ public async Task Should_Run_Teardown_After_Last_Running_Task_Even_If_Task_Faile // Then Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Fail", result?.Message); Assert.Contains(fixture.Log.Entries, x => x.Message == "Executing custom teardown action..."); } @@ -849,7 +849,7 @@ public async Task Should_Run_Teardown_If_Setup_Failed() // Then Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Fail", result?.Message); Assert.Contains(fixture.Log.Entries, x => x.Message == "Executing custom teardown action..."); } @@ -872,7 +872,7 @@ public async Task Should_Throw_Exception_Thrown_From_Setup_Action_If_Both_Setup_ // Then Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Setup", result?.Message); } @@ -988,7 +988,7 @@ public async Task Should_Log_Teardown_Exception_If_Both_Setup_And_Teardown_Actio engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings)); // Then - Assert.IsType(result); + Assert.IsType(result); Assert.Contains(fixture.Log.Entries, x => x.Message.StartsWith("Teardown error: Teardown #1")); Assert.Contains(fixture.Log.Entries, x => x.Message.StartsWith("Teardown error: Teardown #2")); } @@ -1010,7 +1010,7 @@ public async Task Should_Throw_Exception_Thrown_From_Task_If_Both_Task_And_Teard // Then Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Task", result?.Message); } @@ -1032,7 +1032,7 @@ public async Task Should_Log_Teardown_Exception_If_Both_Task_And_Teardown_Action engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings)); // Then - Assert.IsType(result); + Assert.IsType(result); Assert.Contains(fixture.Log.Entries, x => x.Message.StartsWith("Teardown error: Teardown #1")); Assert.Contains(fixture.Log.Entries, x => x.Message.StartsWith("Teardown error: Teardown #2")); } @@ -1291,7 +1291,7 @@ public async Task Should_Run_Task_Teardown_After_Each_Running_Task_Even_If_Task_ // Then Assert.NotNull(exception); - Assert.IsType(exception); + Assert.IsType(exception); Assert.Equal("Fail", exception?.Message); Assert.Equal( new List @@ -1326,7 +1326,7 @@ public async Task Should_Run_Task_Teardown_If_Task_Setup_Failed() // Then Assert.NotNull(exception); - Assert.IsType(exception); + Assert.IsType(exception); Assert.Equal("Fail", exception?.Message); Assert.Equal( new List @@ -1355,7 +1355,7 @@ public async Task Should_Throw_Exception_Thrown_From_Task_Setup_Action_If_Both_T // Then Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Task Setup: A", result?.Message); } @@ -1377,7 +1377,7 @@ public async Task Should_Throw_Exception_Occurring_In_Task_Teardown_If_No_Previo // Then Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Task Teardown: A", result?.Message); } @@ -1401,7 +1401,7 @@ public async Task Should_Log_Task_Teardown_Exception_If_Both_Task_Setup_And_Task // Then Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Task Setup: A", result?.Message); Assert.Contains(fixture.Log.Entries, x => x.Message.StartsWith("Task Teardown error (A):")); } @@ -1424,7 +1424,7 @@ public async Task Should_Log_Exception_Thrown_From_Task_If_Both_Task_And_Task_Te // Then Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Task: A", result?.Message); } @@ -1691,7 +1691,7 @@ public async Task Should_Throw_If_Any_Target_Task_Is_Skipped() engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings)); // Then - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Could not reach target 'B' since it was skipped due to a criteria.", result?.Message); } @@ -1762,7 +1762,7 @@ public async Task Should_Not_Catch_Exceptions_From_Task_If_ContinueOnError_Is_No engine.RunTargetAsync(fixture.Context, fixture.ExecutionStrategy, settings)); // Then - Assert.IsType(result); + Assert.IsType(result); Assert.Equal("Whoopsie", result?.Message); } diff --git a/src/Cake.Core/CakeEngine.cs b/src/Cake.Core/CakeEngine.cs index 81e779840e..72a77361a0 100644 --- a/src/Cake.Core/CakeEngine.cs +++ b/src/Cake.Core/CakeEngine.cs @@ -205,7 +205,8 @@ public async Task RunTargetAsync(ICakeContext context, IExecutionStr { exceptionWasThrown = true; thrownException = ex; - throw; + + throw new CakeReportException(report, ex.Message, ex); } finally { @@ -351,20 +352,20 @@ private async Task ExecuteTaskAsync(ICakeContext context, IExecutionStrategy str PerformTaskTeardown(context, strategy, task, stopWatch.Elapsed, false, taskException); _log.Verbose($"Completed in {stopWatch.Elapsed}"); - } - // Add the task results to the report - if (IsDelegatedTask(task)) - { - report.AddDelegated(task.Name, stopWatch.Elapsed); - } - else if (taskException is null) - { - report.Add(task.Name, CakeReportEntryCategory.Task, stopWatch.Elapsed); - } - else - { - report.AddFailed(task.Name, stopWatch.Elapsed); + // Add the task results to the report + if (IsDelegatedTask(task)) + { + report.AddDelegated(task.Name, stopWatch.Elapsed); + } + else if (taskException is null) + { + report.Add(task.Name, CakeReportEntryCategory.Task, stopWatch.Elapsed); + } + else + { + report.AddFailed(task.Name, stopWatch.Elapsed); + } } } diff --git a/src/Cake.Core/CakeReportException.cs b/src/Cake.Core/CakeReportException.cs new file mode 100644 index 0000000000..e5cdf9f75b --- /dev/null +++ b/src/Cake.Core/CakeReportException.cs @@ -0,0 +1,77 @@ +// 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; + +namespace Cake.Core +{ + /// + /// Represent errors that occur during script execution. + /// + public sealed class CakeReportException : Exception + { + /// + /// Gets or sets the Cake Report. + /// + public CakeReport Report { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public CakeReportException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public CakeReportException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. + public CakeReportException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Cake Report. + public CakeReportException(CakeReport report) + { + Report = report; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Cake Report. + /// The error message that explains the reason for the exception. + public CakeReportException(CakeReport report, string message) + : base(message) + { + Report = report; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Cake Report. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. + public CakeReportException(CakeReport report, string message, Exception innerException) + : base(message, innerException) + { + Report = report; + } + } +} \ No newline at end of file diff --git a/src/Cake.Core/CakeReportPrinter.cs b/src/Cake.Core/CakeReportPrinter.cs index 07e495482c..a8b561a227 100644 --- a/src/Cake.Core/CakeReportPrinter.cs +++ b/src/Cake.Core/CakeReportPrinter.cs @@ -49,13 +49,13 @@ public void Write(CakeReport report) } maxTaskNameLength++; - string lineFormat = "{0,-" + maxTaskNameLength + "}{1,-20}"; + string lineFormat = "{0,-" + maxTaskNameLength + "}{1,-20}{2,-20}"; _console.ForegroundColor = ConsoleColor.Green; // Write header. _console.WriteLine(); - _console.WriteLine(lineFormat, "Task", "Duration"); - _console.WriteLine(new string('-', 20 + maxTaskNameLength)); + _console.WriteLine(lineFormat, "Task", "Duration", "Status"); + _console.WriteLine(new string('-', 40 + maxTaskNameLength)); // Write task status. foreach (var item in report) @@ -63,14 +63,14 @@ public void Write(CakeReport report) if (ShouldWriteTask(item)) { _console.ForegroundColor = GetItemForegroundColor(item); - _console.WriteLine(lineFormat, item.TaskName, FormatDuration(item)); + _console.WriteLine(lineFormat, item.TaskName, FormatDuration(item), item.ExecutionStatus); } } // Write footer. _console.ForegroundColor = ConsoleColor.Green; - _console.WriteLine(new string('-', 20 + maxTaskNameLength)); - _console.WriteLine(lineFormat, "Total:", FormatTime(GetTotalTime(report))); + _console.WriteLine(new string('-', 40 + maxTaskNameLength)); + _console.WriteLine(lineFormat, "Total:", FormatTime(GetTotalTime(report)), string.Empty); } finally { @@ -134,7 +134,7 @@ private string FormatDuration(CakeReportEntry item) { if (item.ExecutionStatus == CakeTaskExecutionStatus.Skipped) { - return "Skipped"; + return "-"; } return FormatTime(item.Duration);