Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public async Task Verify_that_InvokeAsync_returns_0()
x => x.Match(It.IsAny<IEnumerable<Requirement>>(), It.IsAny<IEnumerable<TestCase>>()),
Times.Once);

this.reportGenerator.Verify(x => x.Generate(It.IsAny<IEnumerable<Requirement>>(), It.IsAny<string>(), ReportKind.SpreadSheet),
this.reportGenerator.Verify(x => x.Generate(It.IsAny<IEnumerable<Requirement>>(), It.IsAny<string>(), ReportKind.SpreadSheet, It.IsAny<bool>()),
Times.Once);

Assert.That(result, Is.EqualTo(0));
Expand Down
55 changes: 55 additions & 0 deletions VCD-Generator.Tests/Services/ReportGeneratorTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,60 @@ public void Verify_that_html_report_is_not_generated_and_exception_is_thrown()
Assert.That(() => this.reportGenerator.Generate(this.requirements, this.spreadsheetReportPath, ReportKind.Html),
Throws.TypeOf<NotImplementedException>());
}

[Test]
public void Verify_that_spreadsheet_report_is_generated_with_status_column()
{
Assert.That(() => this.reportGenerator.Generate(this.requirements, this.spreadsheetReportPath, ReportKind.SpreadSheet, addStatusColumn: true),
Throws.Nothing);
}

[Test]
public void Verify_that_DeriveStatus_returns_empty_for_uncovered_requirement()
{
var requirement = new Requirement { Identifier = "REQ-99" };

Assert.That(ReportGenerator.DeriveStatus(requirement), Is.EqualTo(string.Empty));
}

[Test]
public void Verify_that_DeriveStatus_returns_Passed_when_every_test_case_is_Passed()
{
var requirement = new Requirement { Identifier = "REQ-99" };
requirement.TestCases.Add(new TestCase { FullName = "T1", Result = "Passed" });
requirement.TestCases.Add(new TestCase { FullName = "T2", Result = "Passed" });

Assert.That(ReportGenerator.DeriveStatus(requirement), Is.EqualTo("Passed"));
}

[Test]
public void Verify_that_DeriveStatus_returns_Failed_when_any_test_case_is_Failed()
{
var requirement = new Requirement { Identifier = "REQ-99" };
requirement.TestCases.Add(new TestCase { FullName = "T1", Result = "Passed" });
requirement.TestCases.Add(new TestCase { FullName = "T2", Result = "Failed" });

Assert.That(ReportGenerator.DeriveStatus(requirement), Is.EqualTo("Failed"));
}

[Test]
public void Verify_that_DeriveStatus_returns_Mixed_when_Passed_mixes_with_non_Passed_non_Failed()
{
var requirement = new Requirement { Identifier = "REQ-99" };
requirement.TestCases.Add(new TestCase { FullName = "T1", Result = "Passed" });
requirement.TestCases.Add(new TestCase { FullName = "T2", Result = "Inconclusive" });

Assert.That(ReportGenerator.DeriveStatus(requirement), Is.EqualTo("Mixed"));
}

[Test]
public void Verify_that_DeriveStatus_returns_Inconclusive_when_no_Passed_and_no_Failed()
{
var requirement = new Requirement { Identifier = "REQ-99" };
requirement.TestCases.Add(new TestCase { FullName = "T1", Result = "Inconclusive" });
requirement.TestCases.Add(new TestCase { FullName = "T2", Result = "Skipped" });

Assert.That(ReportGenerator.DeriveStatus(requirement), Is.EqualTo("Inconclusive"));
}
}
}
21 changes: 20 additions & 1 deletion VCD-Generator/Commands/GenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ public class GenerateCommand : RootCommand
/// </summary>
public Option<FileInfo> OutputReportOption { get; }

/// <summary>
/// The <see cref="Option{T}"/> that controls whether an aggregated, colour-coded
/// <c>STATUS</c> column is appended to the report.
/// </summary>
public Option<bool> AddStatusColumnOption { get; }

/// <summary>
/// Initializes a new instance of the <see cref="GenerateCommand"/>
/// </summary>
Expand Down Expand Up @@ -127,6 +133,12 @@ public GenerateCommand() : base("VCD Generator")
Required = true,
};
this.Options.Add(this.OutputReportOption);

this.AddStatusColumnOption = new Option<bool>("--add-status-column", "-st")
{
Description = "When set, appends a STATUS column to the report with an aggregated, colour-coded per-requirement outcome (empty = uncovered, Passed = green, Failed = red, Mixed = orange, Inconclusive = grey). Default: false.",
};
this.Options.Add(this.AddStatusColumnOption);
}

/// <summary>
Expand All @@ -147,6 +159,7 @@ public void BindTo(Handler handler, ParseResult parseResult)
handler.RequirementsTextColumn = parseResult.GetValue(this.RequirementsTextColumnOption);
handler.SourceDirectory = parseResult.GetValue(this.SourceDirectoryOption);
handler.OutputReport = parseResult.GetValue(this.OutputReportOption);
handler.AddStatusColumn = parseResult.GetValue(this.AddStatusColumnOption);
}

/// <summary>
Expand Down Expand Up @@ -246,6 +259,12 @@ public Handler(IRequirementsReader requirementsReader, ITestResultReader resultR
/// </summary>
public FileInfo OutputReport { get; set; }

/// <summary>
/// Gets or sets a value indicating whether an aggregated, colour-coded <c>STATUS</c>
/// column is appended to the report.
/// </summary>
public bool AddStatusColumn { get; set; }

/// <summary>
/// Asynchronously executes the command
/// </summary>
Expand Down Expand Up @@ -331,7 +350,7 @@ await AnsiConsole.Status()
ctx.Status($"Generating report at Warp 11, Captain..., SLOW DOWN!");
Thread.Sleep(1500);

this.reportGenerator.Generate(requirements, this.OutputReport.FullName, ReportKind.SpreadSheet);
this.reportGenerator.Generate(requirements, this.OutputReport.FullName, ReportKind.SpreadSheet, this.AddStatusColumn);
AnsiConsole.MarkupLine($"[grey]LOG:[/] VCD report generated at [bold]{this.OutputReport.FullName}[/]");

return Task.FromResult(0);
Expand Down
10 changes: 8 additions & 2 deletions VCD-Generator/Services/IReportGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ public interface IReportGenerator
/// The <see cref="Requirement"/> objects on the basis of which the report will be generated
/// </param>
/// <param name="filePath">
/// the file path(including file-name) where the report will be generated
/// the file path(including file-name) where the report will be generated
/// </param>
/// <param name="reportKind">
/// The kind of report that is generated
/// </param>
void Generate(IEnumerable<Requirement> requirements, string filePath, ReportKind reportKind);
/// <param name="addStatusColumn">
/// When <c>true</c>, an additional <c>STATUS</c> column is written to the report with an
/// aggregated per-requirement outcome (empty / Passed / Failed / Mixed / Inconclusive) and
/// a matching background colour on the cell. Defaults to <c>false</c> to preserve the
/// pre-existing report schema.
/// </param>
void Generate(IEnumerable<Requirement> requirements, string filePath, ReportKind reportKind, bool addStatusColumn = false);
}
}
121 changes: 112 additions & 9 deletions VCD-Generator/Services/ReportGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace VCD.Generator.Services
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;

using ClosedXML.Excel;
Expand Down Expand Up @@ -61,17 +62,21 @@ public ReportGenerator(ILoggerFactory loggerFactory = null)
/// The <see cref="Requirement"/> objects on the basis of which the report will be generated
/// </param>
/// <param name="filePath">
/// the file path (including file-name) where the report will be generated
/// the file path (including file-name) where the report will be generated
/// </param>
/// <param name="reportKind">
/// The kind of report that is generated
/// </param>
public void Generate(IEnumerable<Requirement> requirements, string filePath, ReportKind reportKind)
/// <param name="addStatusColumn">
/// When <c>true</c>, a <c>STATUS</c> column with an aggregated, colour-coded outcome per
/// requirement is appended to the report. See <see cref="DeriveStatus"/> for the rules.
/// </param>
public void Generate(IEnumerable<Requirement> requirements, string filePath, ReportKind reportKind, bool addStatusColumn = false)
{
switch (reportKind)
{
case ReportKind.SpreadSheet:
this.GenerateSpreadsheetReport(requirements,filePath);
this.GenerateSpreadsheetReport(requirements, filePath, addStatusColumn);
break;
case ReportKind.Html:
this.GeneratedHtmlReport(requirements, filePath);
Expand All @@ -86,29 +91,39 @@ public void Generate(IEnumerable<Requirement> requirements, string filePath, Rep
/// The <see cref="Requirement"/> objects on the basis of which the report will be generated
/// </param>
/// <param name="filePath">
/// the file path (including file-name) where the report will be generated
/// the file path (including file-name) where the report will be generated
/// </param>
/// <param name="addStatusColumn">
/// When <c>true</c>, appends a <c>STATUS</c> column with the colour-coded per-requirement
/// outcome; see <see cref="DeriveStatus"/>.
/// </param>
private void GenerateSpreadsheetReport(IEnumerable<Requirement> requirements, string filePath)
private void GenerateSpreadsheetReport(IEnumerable<Requirement> requirements, string filePath, bool addStatusColumn)
{
this.logger.LogInformation("Creating target workbook");

var wb = new XLWorkbook();

var now = DateTime.UtcNow.ToString("yyyy-MM-dd");

var worksheet = wb.Worksheets.Add($"VCD-{now}");

var dataTable = new DataTable();
dataTable.Columns.Add("REQUIREMENT-ID", typeof(string));
dataTable.Columns.Add("REQUIREMENT-TEXT", typeof(string));
dataTable.Columns.Add("TESTCASES", typeof(string));

foreach (var requirement in requirements)
if (addStatusColumn)
{
dataTable.Columns.Add("STATUS", typeof(string));
}

var materialisedRequirements = requirements as IList<Requirement> ?? requirements.ToList();

foreach (var requirement in materialisedRequirements)
{
var dataRow = dataTable.NewRow();
dataRow["REQUIREMENT-ID"] = requirement.Identifier;
dataRow["REQUIREMENT-TEXT"] = requirement.Text;

var sb = new StringBuilder();

foreach (var tc in requirement.TestCases)
Expand All @@ -118,6 +133,11 @@ private void GenerateSpreadsheetReport(IEnumerable<Requirement> requirements, st

dataRow["TESTCASES"] = sb.ToString();

if (addStatusColumn)
{
dataRow["STATUS"] = DeriveStatus(requirement);
}

dataTable.Rows.Add(dataRow);
}

Expand All @@ -130,6 +150,19 @@ private void GenerateSpreadsheetReport(IEnumerable<Requirement> requirements, st
worksheet.Column("A").Width = 25;
worksheet.Column("B").Width = 80;

if (addStatusColumn)
{
const int statusColumnIndex = 4;
worksheet.Column(statusColumnIndex).Style.Alignment.WrapText = false;
worksheet.Column(statusColumnIndex).Width = 15;

for (var i = 0; i < materialisedRequirements.Count; i++)
{
var status = (string)dataTable.Rows[i]["STATUS"];
ApplyStatusFill(worksheet.Cell(i + 2, statusColumnIndex), status);
}
}

try
{
worksheet.Rows().AdjustToContents();
Expand All @@ -144,6 +177,76 @@ private void GenerateSpreadsheetReport(IEnumerable<Requirement> requirements, st
this.logger.LogInformation("Target workbook saved to: {filePath}", filePath);
}

/// <summary>
/// Aggregates the outcome of every <see cref="TestCase"/> linked to a requirement into a
/// single status value suitable for filtering.
/// </summary>
/// <param name="requirement">
/// The <see cref="Requirement"/> whose test cases are aggregated.
/// </param>
/// <returns>
/// <list type="bullet">
/// <item><description><c>""</c> — no test cases (the requirement is uncovered)</description></item>
/// <item><description><c>"Failed"</c> — any test case has <c>Result == "Failed"</c></description></item>
/// <item><description><c>"Passed"</c> — every test case has <c>Result == "Passed"</c></description></item>
/// <item><description><c>"Mixed"</c> — at least one Passed and at least one non-Passed non-Failed</description></item>
/// <item><description><c>"Inconclusive"</c> — no Passed and no Failed (e.g. only Skipped/Inconclusive)</description></item>
/// </list>
/// </returns>
public static string DeriveStatus(Requirement requirement)
{
if (requirement?.TestCases == null || requirement.TestCases.Count == 0)
{
return string.Empty;
}

if (requirement.TestCases.Any(tc => tc.Result == "Failed"))
{
return "Failed";
}

if (requirement.TestCases.All(tc => tc.Result == "Passed"))
{
return "Passed";
}

if (requirement.TestCases.Any(tc => tc.Result == "Passed"))
{
return "Mixed";
}

return "Inconclusive";
}

/// <summary>
/// Applies the conventional Excel "Good/Bad/Neutral" fill palette to a status cell so the
/// report is scannable at a glance.
/// </summary>
/// <param name="cell">
/// The <see cref="IXLCell"/> to colour.
/// </param>
/// <param name="status">
/// The aggregated status value produced by <see cref="DeriveStatus"/>.
/// </param>
private static void ApplyStatusFill(IXLCell cell, string status)
{
switch (status)
{
case "Passed":
cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#C6EFCE"); // light green
break;
case "Failed":
cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#FFC7CE"); // light red
break;
case "Mixed":
cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#FFEB9C"); // light orange
break;
case "Inconclusive":
cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#D9D9D9"); // light grey
break;
}
}

/// <summary>
/// Generates a HTML Report
/// </summary>
Expand Down
Loading