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 @@
+