From c282a4888bcd34af0b6a2bd255a847d5d5ae91eb Mon Sep 17 00:00:00 2001 From: Fabian Tax Date: Fri, 24 Apr 2026 12:04:08 +0200 Subject: [PATCH] [Add] CSV requirements input and VCD report output via Sep --- VCD-Generator.Tests/Data/requirements.csv | 6 + .../Services/CsvReportGeneratorTestFixture.cs | 200 ++++++++++++++++++ .../CsvRequirementsReaderTestFixture.cs | 137 ++++++++++++ .../Services/ReportGeneratorTestFixture.cs | 4 +- ...RequirementsReaderDispatcherTestFixture.cs | 119 +++++++++++ .../VCD-Generator.Tests.csproj | 3 + VCD-Generator/Commands/GenerateCommand.cs | 6 +- VCD-Generator/Program.cs | 5 +- VCD-Generator/Services/CsvReportGenerator.cs | 82 +++++++ .../Services/CsvRequirementsReader.cs | 138 ++++++++++++ VCD-Generator/Services/ReportGenerator.cs | 14 +- VCD-Generator/Services/ReportKind.cs | 5 + .../Services/RequirementsReaderDispatcher.cs | 116 ++++++++++ VCD-Generator/VCD-Generator.csproj | 1 + 14 files changed, 832 insertions(+), 4 deletions(-) create mode 100644 VCD-Generator.Tests/Data/requirements.csv create mode 100644 VCD-Generator.Tests/Services/CsvReportGeneratorTestFixture.cs create mode 100644 VCD-Generator.Tests/Services/CsvRequirementsReaderTestFixture.cs create mode 100644 VCD-Generator.Tests/Services/RequirementsReaderDispatcherTestFixture.cs create mode 100644 VCD-Generator/Services/CsvReportGenerator.cs create mode 100644 VCD-Generator/Services/CsvRequirementsReader.cs create mode 100644 VCD-Generator/Services/RequirementsReaderDispatcher.cs diff --git a/VCD-Generator.Tests/Data/requirements.csv b/VCD-Generator.Tests/Data/requirements.csv new file mode 100644 index 0000000..efce04b --- /dev/null +++ b/VCD-Generator.Tests/Data/requirements.csv @@ -0,0 +1,6 @@ +Identifier,Requirement Text,Section Nr,Main Section,WP,Priority,User Journey,Verif Method,Compliance (Y/N/P),Actual Compliance(Y/N/P),Close out status,Comments +REQ-01,The VCD Generator shall read the test results created by the NunitXml.TestLogger in the Nunit3 output format,1.1,VCD Generation,1,high,User Journey,Test,Y,Y,-,- + REQ-02 ,"Requirement text, with an embedded comma for RFC-4180 quoting",1.2,VCD Generation,2,high,User Journey,Test,Y,Y,-,- +,Row with empty identifier should be skipped by the importer,1.3,VCD Generation,3,high,User Journey,Test,Y,Y,-,- +REQ-03,"Line one of requirement +Line two continues here",1.4,VCD Generation,4,high,User Journey,Test,Y,Y,-,- diff --git a/VCD-Generator.Tests/Services/CsvReportGeneratorTestFixture.cs b/VCD-Generator.Tests/Services/CsvReportGeneratorTestFixture.cs new file mode 100644 index 0000000..ec39f85 --- /dev/null +++ b/VCD-Generator.Tests/Services/CsvReportGeneratorTestFixture.cs @@ -0,0 +1,200 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2024 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace VCD.Generator.Tests.Services +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + + using Microsoft.Extensions.Logging; + + using nietras.SeparatedValues; + + using NUnit.Framework; + + using VCD.Generator.Services; + + /// + /// Suite of tests for the class + /// + [TestFixture] + public class CsvReportGeneratorTestFixture + { + private CsvReportGenerator csvReportGenerator; + + private List requirements; + + private string csvReportPath; + + private ILoggerFactory loggerFactory; + + [SetUp] + public void SetUp() + { + this.loggerFactory = LoggerFactory.Create(builder => + builder.AddConsole().SetMinimumLevel(LogLevel.Trace)); + + this.csvReportPath = Path.Combine(Path.GetTempPath(), System.Guid.NewGuid().ToString("N") + ".csv"); + + this.csvReportGenerator = new CsvReportGenerator(this.loggerFactory); + + this.CreateTestData(); + } + + [TearDown] + public void TearDown() + { + if (File.Exists(this.csvReportPath)) + { + File.Delete(this.csvReportPath); + } + } + + private void CreateTestData() + { + this.requirements = new List(); + + var requirement_1 = new Requirement + { + Identifier = "REQ-01", + Text = "The VCD Generator shall read the test results created by the NunitXml.TestLogger in the Nunit3 output format" + }; + this.requirements.Add(requirement_1); + + var requirement_2 = new Requirement + { + Identifier = "REQ-02", + Text = "The VCD Generator shall read the requirements from an excel spreadsheet" + }; + this.requirements.Add(requirement_2); + + var testCase_1_1 = new TestCase + { + Name = "Verify_that_Read_throws_exception", + FullName = "VCD.Generator.Tests.Services.TestResultReaderTestFixture.Verify_that_Read_throws_exception", + Description = "Verifies that the TestResultReader.Read methods trows an exception", + RequirementId = new List { "REQ-01" }, + Result = "Passed" + }; + requirement_1.TestCases.Add(testCase_1_1); + + var testCase_1_2 = new TestCase + { + Name = "Verify_that_Read_does_not_throw_exception", + FullName = "VCD.Generator.Tests.Services.TestResultReaderTestFixture.Verify_that_Read_does_not_throw_exception", + Description = "Verifies that the TestResultReader.Read methods does not throw an exception", + RequirementId = new List { "REQ-01" }, + Result = "Passed" + }; + requirement_1.TestCases.Add(testCase_1_2); + + var testCase_2 = new TestCase + { + Name = "Verify_that_Read_throws_exception", + FullName = "VCD.Generator.Tests.Services.RequirementsReaderTestFixture.Verify_that_Read_throws_exception", + Description = "Verifies that the TestResultReader.Read methods trows an exception", + RequirementId = new List { "REQ-02" }, + Result = "Failed" + }; + requirement_2.TestCases.Add(testCase_2); + } + + [Test] + public void Verify_that_Generate_writes_header_row_and_rows() + { + this.csvReportGenerator.Generate(this.requirements, this.csvReportPath); + + using var reader = Sep.Reader().FromFile(this.csvReportPath); + + var header = reader.Header.ColNames.ToArray(); + Assert.That(header, Is.EqualTo(new[] { "REQUIREMENT-ID", "REQUIREMENT-TEXT", "TESTCASES" })); + + var rows = new List(); + foreach (var row in reader) + { + rows.Add(new[] { row[0].ToString(), row[1].ToString(), row[2].ToString() }); + } + + Assert.That(rows.Count, Is.EqualTo(this.requirements.Count)); + + for (var i = 0; i < this.requirements.Count; i++) + { + Assert.That(rows[i][0], Is.EqualTo(this.requirements[i].Identifier)); + } + } + + [Test] + public void Verify_that_TESTCASES_cell_contains_embedded_newlines() + { + var requirement = this.requirements[0]; + var singleRequirement = new List { requirement }; + + this.csvReportGenerator.Generate(singleRequirement, this.csvReportPath); + + using var reader = Sep.Reader(o => o with { Unescape = true }).FromFile(this.csvReportPath); + + var testCasesCells = new List(); + foreach (var row in reader) + { + testCasesCells.Add(row["TESTCASES"].ToString()); + } + + Assert.That(testCasesCells.Count, Is.EqualTo(1)); + + var tc1 = requirement.TestCases[0]; + var tc2 = requirement.TestCases[1]; + var expected = $"{tc1.FullName} - {tc1.Result}\n{tc2.FullName} - {tc2.Result}"; + + Assert.That(testCasesCells[0], Is.EqualTo(expected)); + } + + [Test] + public void Verify_that_Generate_overwrites_existing_file() + { + this.csvReportGenerator.Generate(this.requirements, this.csvReportPath); + + var secondBatch = new List + { + new Requirement + { + Identifier = "REQ-99", + Text = "Overwrite requirement" + } + }; + + this.csvReportGenerator.Generate(secondBatch, this.csvReportPath); + + using var reader = Sep.Reader().FromFile(this.csvReportPath); + + var ids = new List(); + var texts = new List(); + foreach (var row in reader) + { + ids.Add(row["REQUIREMENT-ID"].ToString()); + texts.Add(row["REQUIREMENT-TEXT"].ToString()); + } + + Assert.That(ids.Count, Is.EqualTo(1)); + Assert.That(ids[0], Is.EqualTo("REQ-99")); + Assert.That(texts[0], Is.EqualTo("Overwrite requirement")); + } + } +} diff --git a/VCD-Generator.Tests/Services/CsvRequirementsReaderTestFixture.cs b/VCD-Generator.Tests/Services/CsvRequirementsReaderTestFixture.cs new file mode 100644 index 0000000..62d4578 --- /dev/null +++ b/VCD-Generator.Tests/Services/CsvRequirementsReaderTestFixture.cs @@ -0,0 +1,137 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2024 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace VCD.Generator.Tests.Services +{ + using System.Linq; + using System.IO; + + using Microsoft.Extensions.Logging; + + using NUnit.Framework; + + using VCD.Generator.Services; + + /// + /// Suite of tests for the class + /// + [TestFixture] + public class CsvRequirementsReaderTestFixture + { + private CsvRequirementsReader requirementsReader; + + private FileInfo requirementsDocumentFileInfo; + + private ILoggerFactory loggerFactory; + + [SetUp] + public void SetUp() + { + this.loggerFactory = LoggerFactory.Create(builder => + builder.AddConsole().SetMinimumLevel(LogLevel.Trace)); + + this.requirementsDocumentFileInfo = new FileInfo(Path.Combine(TestContext.CurrentContext.WorkDirectory, + "Data", "requirements.csv")); + + this.requirementsReader = new CsvRequirementsReader(this.loggerFactory); + } + + [Test(Description = "Verifies that the CsvRequirementsReader.Read method returns the expected requirements"), + Property("REQUIREMENT-ID", "REQ-02")] + public void Verify_that_Read_with_default_first_column_returns_expected_requirements() + { + var requirements = this.requirementsReader.Read(this.requirementsDocumentFileInfo).ToList(); + + Assert.That(requirements, Is.Not.Empty); + Assert.That(requirements.Select(x => x.Identifier).ToList(), + Is.EqualTo(new[] { "REQ-01", "REQ-02", "REQ-03" })); + } + + [Test] + public void Verify_that_Read_with_named_identifier_column_returns_expected_requirements() + { + var requirements = this.requirementsReader + .Read(this.requirementsDocumentFileInfo, null, "Identifier").ToList(); + + Assert.That(requirements.Select(x => x.Identifier).ToList(), + Is.EqualTo(new[] { "REQ-01", "REQ-02", "REQ-03" })); + } + + [Test] + public void Verify_that_Read_with_named_text_column_populates_Text() + { + var requirements = this.requirementsReader + .Read(this.requirementsDocumentFileInfo, null, "Identifier", "Requirement Text").ToList(); + + Assert.That(requirements.Any(r => !string.IsNullOrEmpty(r.Text)), Is.True); + + var multiline = requirements.Single(r => r.Identifier == "REQ-03"); + Assert.That(multiline.Text, Does.Contain("\n")); + Assert.That(multiline.Text, Does.Contain("Line one of requirement")); + Assert.That(multiline.Text, Does.Contain("Line two continues here")); + } + + [Test] + public void Verify_that_Read_with_unknown_identifier_column_throws_InvalidRequirementsFormatException() + { + Assert.That( + () => this.requirementsReader + .Read(this.requirementsDocumentFileInfo, null, "NotAColumn").ToList(), + Throws.TypeOf()); + } + + [Test] + public void Verify_that_Read_with_unknown_text_column_throws_InvalidRequirementsFormatException() + { + Assert.That( + () => this.requirementsReader + .Read(this.requirementsDocumentFileInfo, null, "Identifier", "NotAColumn").ToList(), + Throws.TypeOf()); + } + + [Test] + public void Verify_that_empty_identifier_rows_are_skipped() + { + var requirements = this.requirementsReader.Read(this.requirementsDocumentFileInfo).ToList(); + + Assert.That(requirements.Count, Is.EqualTo(3)); + } + + [Test] + public void Verify_that_identifier_is_trimmed() + { + var requirements = this.requirementsReader.Read(this.requirementsDocumentFileInfo).ToList(); + + foreach (var requirement in requirements) + { + Assert.That(requirement.Identifier, Is.EqualTo(requirement.Identifier.Trim())); + } + } + + [Test] + public void Verify_that_sheetName_is_ignored_for_csv_input() + { + var requirements = this.requirementsReader + .Read(this.requirementsDocumentFileInfo, "AnySheet", "Identifier", "Requirement Text").ToList(); + + Assert.That(requirements.Count, Is.EqualTo(3)); + } + } +} diff --git a/VCD-Generator.Tests/Services/ReportGeneratorTestFixture.cs b/VCD-Generator.Tests/Services/ReportGeneratorTestFixture.cs index 205c1c2..1763a38 100644 --- a/VCD-Generator.Tests/Services/ReportGeneratorTestFixture.cs +++ b/VCD-Generator.Tests/Services/ReportGeneratorTestFixture.cs @@ -52,7 +52,9 @@ public void SetUp() this.spreadsheetReportPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "VCD-report.xlsx"); - this.reportGenerator = new ReportGenerator(this.loggerFactory); + var csvReportGenerator = new CsvReportGenerator(this.loggerFactory); + + this.reportGenerator = new ReportGenerator(csvReportGenerator, this.loggerFactory); this.CreateTestData(); } diff --git a/VCD-Generator.Tests/Services/RequirementsReaderDispatcherTestFixture.cs b/VCD-Generator.Tests/Services/RequirementsReaderDispatcherTestFixture.cs new file mode 100644 index 0000000..790f485 --- /dev/null +++ b/VCD-Generator.Tests/Services/RequirementsReaderDispatcherTestFixture.cs @@ -0,0 +1,119 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace VCD.Generator.Tests.Services +{ + using System.IO; + using System.Linq; + + using Microsoft.Extensions.Logging; + + using NUnit.Framework; + + using VCD.Generator.Services; + + /// + /// Suite of tests for the class. + /// + /// + /// Because the concrete and + /// methods are not virtual, the dispatcher is + /// verified against real input files. Each routing assertion confirms that the expected + /// concrete reader executed by inspecting the shape of the returned requirements, which + /// differs between the xlsx and csv fixtures. + /// + [TestFixture] + public class RequirementsReaderDispatcherTestFixture + { + private RequirementsReaderDispatcher dispatcher; + + private FileInfo xlsxFileInfo; + + private FileInfo csvFileInfo; + + private ILoggerFactory loggerFactory; + + [SetUp] + public void SetUp() + { + this.loggerFactory = LoggerFactory.Create(builder => + builder.AddConsole().SetMinimumLevel(LogLevel.Trace)); + + this.xlsxFileInfo = new FileInfo(Path.Combine(TestContext.CurrentContext.WorkDirectory, + "Data", "Requirements-01.xlsx")); + + this.csvFileInfo = new FileInfo(Path.Combine(TestContext.CurrentContext.WorkDirectory, + "Data", "requirements.csv")); + + var xlsxReader = new RequirementsReader(this.loggerFactory); + var csvReader = new CsvRequirementsReader(this.loggerFactory); + + this.dispatcher = new RequirementsReaderDispatcher(xlsxReader, csvReader, this.loggerFactory); + } + + [Test] + public void Verify_that_csv_extension_routes_to_CsvRequirementsReader() + { + var requirements = this.dispatcher.Read(this.csvFileInfo).ToList(); + + // The csv fixture contains three requirements (REQ-01, REQ-02, REQ-03) + Assert.That(requirements.Count, Is.EqualTo(3)); + Assert.That(requirements.Select(r => r.Identifier).ToList(), + Is.EqualTo(new[] { "REQ-01", "REQ-02", "REQ-03" })); + } + + [Test] + public void Verify_that_xlsx_extension_routes_to_RequirementsReader() + { + var requirements = this.dispatcher.Read(this.xlsxFileInfo).ToList(); + + // The xlsx fixture contains two requirements + Assert.That(requirements.Count, Is.EqualTo(2)); + } + + [Test] + public void Verify_that_uppercase_extension_is_handled() + { + var uppercaseCopyPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, + "Data", "requirements-uppercase.CSV"); + + File.Copy(this.csvFileInfo.FullName, uppercaseCopyPath, true); + + try + { + var uppercaseFileInfo = new FileInfo(uppercaseCopyPath); + + var requirements = this.dispatcher.Read(uppercaseFileInfo).ToList(); + + // Confirms csv routing: shape matches the csv fixture, not the xlsx fixture + Assert.That(requirements.Count, Is.EqualTo(3)); + Assert.That(requirements.Select(r => r.Identifier).ToList(), + Is.EqualTo(new[] { "REQ-01", "REQ-02", "REQ-03" })); + } + finally + { + if (File.Exists(uppercaseCopyPath)) + { + File.Delete(uppercaseCopyPath); + } + } + } + } +} diff --git a/VCD-Generator.Tests/VCD-Generator.Tests.csproj b/VCD-Generator.Tests/VCD-Generator.Tests.csproj index 9da38e3..b17fff0 100644 --- a/VCD-Generator.Tests/VCD-Generator.Tests.csproj +++ b/VCD-Generator.Tests/VCD-Generator.Tests.csproj @@ -59,6 +59,9 @@ Always + + Always + Always diff --git a/VCD-Generator/Commands/GenerateCommand.cs b/VCD-Generator/Commands/GenerateCommand.cs index e5aa9b1..695a9fb 100644 --- a/VCD-Generator/Commands/GenerateCommand.cs +++ b/VCD-Generator/Commands/GenerateCommand.cs @@ -331,7 +331,11 @@ 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); + var reportKind = string.Equals(this.OutputReport.Extension, ".csv", StringComparison.OrdinalIgnoreCase) + ? ReportKind.Csv + : ReportKind.SpreadSheet; + + this.reportGenerator.Generate(requirements, this.OutputReport.FullName, reportKind); AnsiConsole.MarkupLine($"[grey]LOG:[/] VCD report generated at [bold]{this.OutputReport.FullName}[/]"); return Task.FromResult(0); diff --git a/VCD-Generator/Program.cs b/VCD-Generator/Program.cs index 83b0c65..ff7afcd 100644 --- a/VCD-Generator/Program.cs +++ b/VCD-Generator/Program.cs @@ -62,9 +62,12 @@ public static async Task Main(string[] args) .ReadFrom.Configuration(context.Configuration)) .ConfigureServices((hostContext, services) => { - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); }) diff --git a/VCD-Generator/Services/CsvReportGenerator.cs b/VCD-Generator/Services/CsvReportGenerator.cs new file mode 100644 index 0000000..ecbcf4a --- /dev/null +++ b/VCD-Generator/Services/CsvReportGenerator.cs @@ -0,0 +1,82 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2024 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace VCD.Generator.Services +{ + using System.Collections.Generic; + using System.Linq; + + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + + using nietras.SeparatedValues; + + /// + /// The purpose of the is to generate a verification control document + /// report in CSV format. This is a helper class used by and is not + /// intended to be an implementation on its own. + /// + public class CsvReportGenerator + { + /// + /// The used to log + /// + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The (injected) used to setup logging + /// + public CsvReportGenerator(ILoggerFactory loggerFactory = null) + { + this.logger = loggerFactory == null ? NullLogger.Instance : loggerFactory.CreateLogger(); + } + + /// + /// Generates a CSV VCD report at the specified location. + /// + /// + /// The objects on the basis of which the report will be generated + /// + /// + /// the file path (including file-name) where the report will be generated + /// + public void Generate(IEnumerable requirements, string filePath) + { + this.logger.LogInformation("Creating CSV report at {filePath}", filePath); + + using var writer = Sep.Writer(o => o with { Sep = new Sep(','), Escape = true }).ToFile(filePath); + + foreach (var requirement in requirements) + { + var testCases = string.Join("\n", requirement.TestCases.Select(tc => $"{tc.FullName} - {tc.Result}")); + + using var row = writer.NewRow(); + row["REQUIREMENT-ID"].Set(requirement.Identifier); + row["REQUIREMENT-TEXT"].Set(requirement.Text); + row["TESTCASES"].Set(testCases); + } + + this.logger.LogInformation("CSV report saved to: {filePath}", filePath); + } + } +} diff --git a/VCD-Generator/Services/CsvRequirementsReader.cs b/VCD-Generator/Services/CsvRequirementsReader.cs new file mode 100644 index 0000000..21dd2da --- /dev/null +++ b/VCD-Generator/Services/CsvRequirementsReader.cs @@ -0,0 +1,138 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2024 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace VCD.Generator.Services +{ + using System.Collections.Generic; + using System.IO; + + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + + using nietras.SeparatedValues; + + /// + /// The purpose of the is to read the list of requirements from a CSV file + /// + /// + /// The input is a CSV file where the column that holds the requirement identifier (and optionally the + /// requirement text) is specified by header name. The separator is auto-detected by the Sep library. + /// + public class CsvRequirementsReader : IRequirementsReader + { + /// + /// The used to log + /// + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The (injected) used to setup logging + /// + public CsvRequirementsReader(ILoggerFactory loggerFactory = null) + { + this.logger = loggerFactory == null ? NullLogger.Instance : loggerFactory.CreateLogger(); + } + + /// + /// Reads the s from the specified CSV file + /// + /// + /// The to the requirements input file + /// + /// + /// Ignored for CSV input. When a non-empty value is supplied a debug log entry is emitted and the value is discarded. + /// + /// + /// The name of the header column where the unique identifier of the requirements is located. When this is null + /// or empty, the first column is used. + /// + /// + /// The name of the header column where the requirement text is located. When this is null the requirement text + /// is ignored and remains an empty string. + /// + /// + /// A fully materialised + /// + public IEnumerable Read(FileInfo fileInfo, string sheetName = null, string identifierColumnName = null, string textColumnName = null) + { + if (!string.IsNullOrEmpty(sheetName)) + { + this.logger.LogDebug("sheetName '{sheetName}' ignored for CSV input", sheetName); + } + + var results = new List(); + + using var reader = Sep.Reader().FromFile(fileInfo.FullName); + + int requirementIdColumnIndex; + if (string.IsNullOrEmpty(identifierColumnName)) + { + requirementIdColumnIndex = 0; + } + else + { + if (!reader.Header.TryIndexOf(identifierColumnName, out requirementIdColumnIndex)) + { + throw new InvalidRequirementsFormatException($"The identifier column with name \"{identifierColumnName}\" could not be found"); + } + } + + var requirementTextColumnIndex = -1; + if (textColumnName != null) + { + if (!reader.Header.TryIndexOf(textColumnName, out requirementTextColumnIndex)) + { + throw new InvalidRequirementsFormatException($"The text column with name \"{textColumnName}\" could not be found"); + } + } + + foreach (var row in reader) + { + var identifier = row[requirementIdColumnIndex].ToString(); + + if (string.IsNullOrWhiteSpace(identifier)) + { + continue; + } + + var text = string.Empty; + if (textColumnName != null) + { + text = row[requirementTextColumnIndex].ToString(); + } + + this.logger.LogDebug("requirement found: {identifier}", identifier); + + var requirement = new Requirement + { + Identifier = identifier.Trim(), + Text = text + }; + + results.Add(requirement); + } + + return results; + } + } +} diff --git a/VCD-Generator/Services/ReportGenerator.cs b/VCD-Generator/Services/ReportGenerator.cs index 85beeee..6c5341e 100644 --- a/VCD-Generator/Services/ReportGenerator.cs +++ b/VCD-Generator/Services/ReportGenerator.cs @@ -43,14 +43,23 @@ public class ReportGenerator : IReportGenerator /// private readonly ILogger logger; + /// + /// The used to generate CSV reports + /// + private readonly CsvReportGenerator csvReportGenerator; + /// /// Initializes a new instance of the class. /// + /// + /// The (injected) used to generate CSV reports + /// /// /// The (injected) used to setup logging /// - public ReportGenerator(ILoggerFactory loggerFactory = null) + public ReportGenerator(CsvReportGenerator csvReportGenerator, ILoggerFactory loggerFactory = null) { + this.csvReportGenerator = csvReportGenerator ?? throw new ArgumentNullException(nameof(csvReportGenerator)); this.logger = loggerFactory == null ? NullLogger.Instance : loggerFactory.CreateLogger(); } @@ -76,6 +85,9 @@ public void Generate(IEnumerable requirements, string filePath, Rep case ReportKind.Html: this.GeneratedHtmlReport(requirements, filePath); break; + case ReportKind.Csv: + this.csvReportGenerator.Generate(requirements, filePath); + break; } } diff --git a/VCD-Generator/Services/ReportKind.cs b/VCD-Generator/Services/ReportKind.cs index 8b41da9..115b822 100644 --- a/VCD-Generator/Services/ReportKind.cs +++ b/VCD-Generator/Services/ReportKind.cs @@ -30,6 +30,11 @@ public enum ReportKind /// SpreadSheet, + /// + /// Assertion that the report kind is a CSV file + /// + Csv, + /// /// Assertion that the report kind is an HTML /// diff --git a/VCD-Generator/Services/RequirementsReaderDispatcher.cs b/VCD-Generator/Services/RequirementsReaderDispatcher.cs new file mode 100644 index 0000000..54941ef --- /dev/null +++ b/VCD-Generator/Services/RequirementsReaderDispatcher.cs @@ -0,0 +1,116 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace VCD.Generator.Services +{ + using System; + using System.Collections.Generic; + using System.IO; + + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + + /// + /// The purpose of the is to delegate the reading of + /// requirements to the appropriate concrete implementation + /// based on the file extension of the provided input file. + /// + /// + /// Files with a .csv extension are routed to the . + /// Any other extension (including .xlsx, empty or unknown extensions) is routed to the + /// spreadsheet-based . + /// + public class RequirementsReaderDispatcher : IRequirementsReader + { + /// + /// The (injected) used for spreadsheet (xlsx) input + /// + private readonly RequirementsReader xlsxReader; + + /// + /// The (injected) used for CSV input + /// + private readonly CsvRequirementsReader csvReader; + + /// + /// The used to log + /// + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The (injected) used when the input file is an xlsx spreadsheet + /// + /// + /// The (injected) used when the input file is a CSV file + /// + /// + /// The (injected) used to setup logging + /// + /// + /// Thrown when or is null. + /// + public RequirementsReaderDispatcher(RequirementsReader xlsxReader, CsvRequirementsReader csvReader, ILoggerFactory loggerFactory = null) + { + this.xlsxReader = xlsxReader + ?? throw new ArgumentNullException(nameof(xlsxReader)); + this.csvReader = csvReader + ?? throw new ArgumentNullException(nameof(csvReader)); + this.logger = loggerFactory == null ? NullLogger.Instance : loggerFactory.CreateLogger(); + } + + /// + /// Reads the s from the specified file by dispatching to the + /// concrete that matches the file extension. + /// + /// + /// The to the requirements input file + /// + /// + /// The name of the sheet where the requirements are located, in case this is null the first sheet in the + /// workbook is used. Ignored when the input is a CSV file. + /// + /// + /// The name of the column where the unique identifier of the requirements is located, in case + /// this is null, the first used column is used + /// + /// + /// The name of the column where the requirement text is located. In case this is null the requirement text is ignored + /// + /// + /// An + /// + public IEnumerable Read(FileInfo fileInfo, string sheetName = null, string identifierColumnName = null, string textColumnName = null) + { + var extension = fileInfo.Extension.ToLowerInvariant(); + + if (extension == ".csv") + { + this.logger.LogDebug("dispatching {fileName} to {reader}", fileInfo.Name, "CSV"); + return this.csvReader.Read(fileInfo, sheetName, identifierColumnName, textColumnName); + } + + this.logger.LogDebug("dispatching {fileName} to {reader}", fileInfo.Name, "xlsx"); + return this.xlsxReader.Read(fileInfo, sheetName, identifierColumnName, textColumnName); + } + } +} diff --git a/VCD-Generator/VCD-Generator.csproj b/VCD-Generator/VCD-Generator.csproj index 124b433..45b7090 100644 --- a/VCD-Generator/VCD-Generator.csproj +++ b/VCD-Generator/VCD-Generator.csproj @@ -47,6 +47,7 @@ +