Skip to content

Commit

Permalink
(cake-buildGH-4055) Add Spectre.Console based reporter
Browse files Browse the repository at this point in the history
Via a new setting, --Settings_UseSpectreConsoleForConsoleOutput, it is
now possible to output the task headers, and task summary using
Spectre.Console.  This allows for the easy addition of a new column in
the task summary, which includes information about _why_ any given task
has been skipped.

The old CakeReportPrinter is still in play, but some implementation has
been moved from within the DefaultExecutionStrategy to the reporter
class.  Depending on the setting mentioned above, either the
Spectre.Console version of the reporter will be added to the IoC
container, or the old version will be in place.

* fixes cake-build#4055
  • Loading branch information
gep13 authored and devlead committed Nov 7, 2022
1 parent cb58993 commit 03b6647
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 36 deletions.
162 changes: 162 additions & 0 deletions src/Cake.Cli/Infrastructure/CakeSpectreReportPrinter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Cake.Core;
using Cake.Core.Diagnostics;
using Spectre.Console;

namespace Cake.Cli
{
/// <summary>
/// The default report printer.
/// </summary>
public sealed class CakeSpectreReportPrinter : ICakeReportPrinter
{
private readonly IAnsiConsole _console;

/// <summary>
/// Initializes a new instance of the <see cref="CakeSpectreReportPrinter"/> class.
/// </summary>
/// <param name="console">The console.</param>
public CakeSpectreReportPrinter(IAnsiConsole console)
{
_console = console;
}

/// <inheritdoc/>
public void Write(CakeReport report)
{
// Create a table
var table = new Table().Border(TableBorder.SimpleHeavy);
table.Width(100);
table.BorderStyle(new Style().Foreground(ConsoleColor.Green));

var includeSkippedReasonColumn = report.Any(r => !string.IsNullOrEmpty(r.SkippedMessage));
var rowStyle = new Style(ConsoleColor.Green);

// Add some columns
table.AddColumn(new TableColumn(new Text("Task", rowStyle)).Footer(new Text("Total:", rowStyle)).PadRight(10));
table.AddColumn(
new TableColumn(
new Text("Duration", rowStyle)).Footer(
new Text(FormatTime(GetTotalTime(report)), rowStyle)));

if (includeSkippedReasonColumn)
{
table.AddColumn(new TableColumn(new Text("Skip Reason", rowStyle)));
}

foreach (var item in report)
{
var itemStyle = GetItemStyle(item);

if (includeSkippedReasonColumn)
{
table.AddRow(new Markup(item.TaskName, itemStyle),
new Markup(FormatDuration(item), itemStyle),
new Markup(item.SkippedMessage, itemStyle));
}
else
{
table.AddRow(new Markup(item.TaskName, itemStyle),
new Markup(FormatDuration(item), itemStyle));
}
}

// Render the table to the console
_console.Write(table);
}

/// <inheritdoc/>
public void WriteStep(string name, Verbosity verbosity)
{
if (verbosity < Verbosity.Normal)
{
return;
}

var table = new Table().Border(DoubleBorder.Shared);
table.Width(100);
table.AddColumn(name);
_console.Write(new Padder(table).Padding(0, 1, 0, 0));
}

/// <inheritdoc/>
public void WriteLifeCycleStep(string name, Verbosity verbosity)
{
if (verbosity < Verbosity.Normal)
{
return;
}

_console.WriteLine();

var table = new Table().Border(SingleBorder.Shared);
table.Width(100);
table.AddColumn(name);
_console.Write(table);
}

/// <inheritdoc/>
public void WriteSkippedStep(string name, Verbosity verbosity)
{
if (verbosity < Verbosity.Verbose)
{
return;
}

_console.WriteLine();

var table = new Table().Border(DoubleBorder.Shared);
table.Width(100);
table.AddColumn(name);
_console.Write(table);
}

private static string FormatDuration(CakeReportEntry item)
{
if (item.ExecutionStatus == CakeTaskExecutionStatus.Skipped)
{
return "Skipped";
}

return FormatTime(item.Duration);
}

private static Style GetItemStyle(CakeReportEntry item)
{
if (item.Category == CakeReportEntryCategory.Setup || item.Category == CakeReportEntryCategory.Teardown)
{
return new Style(ConsoleColor.Cyan);
}

if (item.ExecutionStatus == CakeTaskExecutionStatus.Failed)
{
return new Style(ConsoleColor.Red);
}

if (item.ExecutionStatus == CakeTaskExecutionStatus.Executed)
{
return new Style(ConsoleColor.Green);
}

return new Style(ConsoleColor.Gray);
}

private static string FormatTime(TimeSpan time)
{
return time.ToString("c", CultureInfo.InvariantCulture);
}

private static TimeSpan GetTotalTime(IEnumerable<CakeReportEntry> entries)
{
return entries.Select(i => i.Duration)
.Aggregate(TimeSpan.Zero, (t1, t2) => t1 + t2);
}
}
}
35 changes: 35 additions & 0 deletions src/Cake.Cli/Infrastructure/DoubleBorder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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 Spectre.Console;
using Spectre.Console.Rendering;

namespace Cake.Cli
{
/// <summary>
/// A custom Spectre.Console border class, used for outputting information about steps.
/// </summary>
public class DoubleBorder : TableBorder
{
/// <summary>
/// Gets a single instance of the DoubleBorder class.
/// </summary>
public static DoubleBorder Shared { get; } = new DoubleBorder();

/// <summary>
/// Get information about the custom border.
/// </summary>
/// <param name="part">The part that needs a border applied to it.</param>
/// <returns>A simple double border character.</returns>
public override string GetPart(TableBorderPart part)
{
return part switch
{
TableBorderPart.HeaderTop => "=",
TableBorderPart.FooterBottom => "=",
_ => string.Empty,
};
}
}
}
35 changes: 35 additions & 0 deletions src/Cake.Cli/Infrastructure/SingleBorder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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 Spectre.Console;
using Spectre.Console.Rendering;

namespace Cake.Cli
{
/// <summary>
/// A custom Spectre.Console border class, used for outputting information about steps.
/// </summary>
public class SingleBorder : TableBorder
{
/// <summary>
/// Gets a single instance of the SingleBorder class.
/// </summary>
public static SingleBorder Shared { get; } = new SingleBorder();

/// <summary>
/// Get information about the custom border.
/// </summary>
/// <param name="part">The part that needs a border applied to it.</param>
/// <returns>A simple single border character.</returns>
public override string GetPart(TableBorderPart part)
{
return part switch
{
TableBorderPart.HeaderTop => "-",
TableBorderPart.FooterBottom => "-",
_ => string.Empty,
};
}
}
}
4 changes: 3 additions & 1 deletion src/Cake.Core.Tests/Fixtures/CakeEngineFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal sealed class CakeEngineFixture
public ICakeArguments Arguments { get; set; }
public IProcessRunner ProcessRunner { get; set; }
public ICakeContext Context { get; set; }
public ICakeReportPrinter ReportPrinter { get; set; }
public IExecutionStrategy ExecutionStrategy { get; set; }
public ICakeDataService DataService { get; set; }

Expand All @@ -28,7 +29,8 @@ public CakeEngineFixture()
Globber = Substitute.For<IGlobber>();
Arguments = Substitute.For<ICakeArguments>();
ProcessRunner = Substitute.For<IProcessRunner>();
ExecutionStrategy = new DefaultExecutionStrategy(Log);
ReportPrinter = Substitute.For<ICakeReportPrinter>();
ExecutionStrategy = new DefaultExecutionStrategy(Log, ReportPrinter);
DataService = Substitute.For<ICakeDataService>();

Context = Substitute.For<ICakeContext>();
Expand Down
10 changes: 5 additions & 5 deletions src/Cake.Core.Tests/Unit/CakeReportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void Should_Add_A_New_Task()
var taskName = "task";

// When
report.AddSkipped(taskName);
report.AddSkipped(taskName, "This task was skipped for a great reason!");

// Then
var firstTask = report.First();
Expand All @@ -75,12 +75,12 @@ public void Should_Add_To_End_Of_Sequence()
{
// Given
var report = new CakeReport();
report.AddSkipped("task 1");
report.AddSkipped("task 1", "This task was skipped for a great reason!");

var taskName = "task 2";

// When
report.AddSkipped(taskName);
report.AddSkipped(taskName, "This task was skipped for a great reason!");

// Then
var lastTask = report.Last();
Expand Down Expand Up @@ -115,7 +115,7 @@ public void Should_Add_To_End_Of_Sequence()
{
// Given
var report = new CakeReport();
report.AddSkipped("task 1");
report.AddSkipped("task 1", "This task was skipped for a great reason!");

var taskName = "task 2";
var duration = TimeSpan.FromMilliseconds(100);
Expand Down Expand Up @@ -156,7 +156,7 @@ public void Should_Add_To_End_Of_Sequence()
{
// Given
var report = new CakeReport();
report.AddSkipped("task 1");
report.AddSkipped("task 1", "This task was skipped for a great reason!");

var taskName = "task 2";
var duration = TimeSpan.FromMilliseconds(100);
Expand Down
13 changes: 12 additions & 1 deletion src/Cake.Core/CakeEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ private async Task RunTask(ICakeContext context, IExecutionStrategy strategy, Ca
{
if (!ShouldTaskExecute(context, task, criteria, isTarget))
{
criteria.CausedSkippingOfTask = true;
SkipTask(context, strategy, task, report, criteria);
skipped = true;
break;
Expand Down Expand Up @@ -437,7 +438,17 @@ private void SkipTask(ICakeContext context, IExecutionStrategy strategy, CakeTas
PerformTaskTeardown(context, strategy, task, TimeSpan.Zero, true, null);

// Add the skipped task to the report.
report.AddSkipped(task.Name);
var skippedTaskCriteria = task.Criterias.FirstOrDefault(c => c.CausedSkippingOfTask == true);
var skippedMessage = string.Empty;
if (skippedTaskCriteria != null)
{
if (!string.IsNullOrEmpty(skippedTaskCriteria.Message))
{
skippedMessage = skippedTaskCriteria.Message;
}
}

report.AddSkipped(task.Name, skippedMessage);
}

private static bool IsDelegatedTask(CakeTask task)
Expand Down
18 changes: 10 additions & 8 deletions src/Cake.Core/CakeReport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public CakeReport()
/// <param name="span">The span.</param>
public void Add(string task, TimeSpan span)
{
Add(task, CakeReportEntryCategory.Task, span, CakeTaskExecutionStatus.Executed);
Add(task, string.Empty, CakeReportEntryCategory.Task, span, CakeTaskExecutionStatus.Executed);
}

/// <summary>
Expand All @@ -49,16 +49,17 @@ public void Add(string task, TimeSpan span)
/// <param name="span">The span.</param>
public void Add(string task, CakeReportEntryCategory category, TimeSpan span)
{
Add(task, category, span, CakeTaskExecutionStatus.Executed);
Add(task, string.Empty, category, span, CakeTaskExecutionStatus.Executed);
}

/// <summary>
/// Adds a skipped task result to the report.
/// </summary>
/// <param name="task">The task.</param>
public void AddSkipped(string task)
/// <param name="skippedMessage">The message explaining why the task was skipped.</param>
public void AddSkipped(string task, string skippedMessage)
{
Add(task, CakeReportEntryCategory.Task, TimeSpan.Zero, CakeTaskExecutionStatus.Skipped);
Add(task, skippedMessage, CakeReportEntryCategory.Task, TimeSpan.Zero, CakeTaskExecutionStatus.Skipped);
}

/// <summary>
Expand All @@ -68,7 +69,7 @@ public void AddSkipped(string task)
/// <param name="span">The span.</param>
public void AddFailed(string task, TimeSpan span)
{
Add(task, CakeReportEntryCategory.Task, span, CakeTaskExecutionStatus.Failed);
Add(task, string.Empty, CakeReportEntryCategory.Task, span, CakeTaskExecutionStatus.Failed);
}

/// <summary>
Expand All @@ -78,19 +79,20 @@ public void AddFailed(string task, TimeSpan span)
/// <param name="span">The span.</param>
public void AddDelegated(string task, TimeSpan span)
{
Add(task, CakeReportEntryCategory.Task, span, CakeTaskExecutionStatus.Delegated);
Add(task, string.Empty, CakeReportEntryCategory.Task, span, CakeTaskExecutionStatus.Delegated);
}

/// <summary>
/// Adds a task result to the report.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="skippedMessage">The message explaining why the task was skipped.</param>
/// <param name="category">The category.</param>
/// <param name="span">The span.</param>
/// <param name="executionStatus">The execution status.</param>
public void Add(string task, CakeReportEntryCategory category, TimeSpan span, CakeTaskExecutionStatus executionStatus)
public void Add(string task, string skippedMessage, CakeReportEntryCategory category, TimeSpan span, CakeTaskExecutionStatus executionStatus)
{
_report.Add(new CakeReportEntry(task, category, span, executionStatus));
_report.Add(new CakeReportEntry(task, skippedMessage, category, span, executionStatus));
}

/// <summary>
Expand Down
Loading

0 comments on commit 03b6647

Please sign in to comment.