diff --git a/ECoreNetto.Reporting.Tests/Generators/CapellaHtmlReportGeneratorTestFixture.cs b/ECoreNetto.Reporting.Tests/Generators/CapellaHtmlReportGeneratorTestFixture.cs index 4779ada..581e597 100644 --- a/ECoreNetto.Reporting.Tests/Generators/CapellaHtmlReportGeneratorTestFixture.cs +++ b/ECoreNetto.Reporting.Tests/Generators/CapellaHtmlReportGeneratorTestFixture.cs @@ -89,6 +89,49 @@ public void Verify_that_the_generated_capella_html_contains_expected_content() }); } + [Test] + public void Verify_that_a_single_combined_html_report_is_generated_for_the_whole_capella_metamodel() + { + var inputDirectory = new DirectoryInfo(CapellaDirectory); + + var reportFileInfo = new FileInfo(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "html-report.capella.combined.html")); + + Assert.That(() => this.htmlReportGenerator.GenerateCombinedReport(inputDirectory, reportFileInfo), Throws.Nothing); + + reportFileInfo.Refresh(); + Assert.That(reportFileInfo.Exists, Is.True); + + var html = File.ReadAllText(reportFileInfo.FullName); + + Assert.Multiple(() => + { + // a single document must contain classifiers that originate from several different .ecore files + Assert.That(html, Does.Contain("CapellaElement")); // CapellaCore.ecore + Assert.That(html, Does.Contain("LogicalArchitecture")); // LogicalArchitecture.ecore + Assert.That(html, Does.Contain("SystemAnalysis")); // ContextArchitecture.ecore + Assert.That(html, Does.Contain("PhysicalArchitecture")); // PhysicalArchitecture.ecore + Assert.That(html, Does.Contain("OperationalAnalysis")); // OperationalAnalysis.ecore + }); + } + + [Test] + public void Verify_that_a_combined_report_from_an_entry_file_includes_reachable_models() + { + // LogicalArchitecture.ecore references classifiers that live in other files (e.g. its super type + // ComponentArchitecture lives in CompositeStructure.ecore); the combined report must include them. + var modelFileInfo = new FileInfo(Path.Combine(CapellaDirectory, "LogicalArchitecture.ecore")); + + var html = this.htmlReportGenerator.GenerateCombinedReport(modelFileInfo); + + Assert.Multiple(() => + { + Assert.That(html, Does.Contain("LogicalArchitecture")); + Assert.That(html, Does.Contain("ComponentArchitecture")); + }); + } + /// /// Enumerates the Capella .ecore file names available in the test output directory. /// diff --git a/ECoreNetto.Reporting.Tests/Generators/CapellaMarkdownReportGeneratorTestFixture.cs b/ECoreNetto.Reporting.Tests/Generators/CapellaMarkdownReportGeneratorTestFixture.cs new file mode 100644 index 0000000..a57be8e --- /dev/null +++ b/ECoreNetto.Reporting.Tests/Generators/CapellaMarkdownReportGeneratorTestFixture.cs @@ -0,0 +1,126 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright 2017-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ECoreNetto.Tools.Tests.Generators +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + + using ECoreNetto.Reporting.Generators; + + using Microsoft.Extensions.Logging; + + using NUnit.Framework; + + using Serilog; + + /// + /// Suite of tests that verify the produces a Markdown report for the + /// Eclipse Capella metamodel (21 cross-referencing .ecore files), both per file and as a single + /// combined report of the whole metamodel. + /// + [TestFixture] + public class CapellaMarkdownReportGeneratorTestFixture + { + private static readonly string CapellaDirectory = Path.Combine( + Path.GetDirectoryName(typeof(CapellaMarkdownReportGeneratorTestFixture).Assembly.Location)!, + "Data", + "capella"); + + private MarkdownReportGenerator markdownReportGenerator = null!; + + private ILoggerFactory? loggerFactory; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Console() + .CreateLogger(); + + this.loggerFactory = LoggerFactory.Create(builder => + { + builder.AddSerilog(); + }); + } + + [SetUp] + public void SetUp() + { + this.markdownReportGenerator = new MarkdownReportGenerator(this.loggerFactory); + } + + [Test] + [TestCaseSource(nameof(CapellaModelFiles))] + public void Verify_that_a_markdown_report_is_generated_for_a_capella_model(string fileName) + { + var modelFileInfo = new FileInfo(Path.Combine(CapellaDirectory, fileName)); + + var reportFileInfo = new FileInfo(Path.Combine( + TestContext.CurrentContext.TestDirectory, + $"markdown-report.capella.{Path.GetFileNameWithoutExtension(fileName)}.md")); + + Assert.That(() => this.markdownReportGenerator.GenerateReport(modelFileInfo, reportFileInfo), Throws.Nothing); + + reportFileInfo.Refresh(); + Assert.That(reportFileInfo.Exists, Is.True); + } + + [Test] + public void Verify_that_a_single_combined_markdown_report_is_generated_for_the_whole_capella_metamodel() + { + var inputDirectory = new DirectoryInfo(CapellaDirectory); + + var reportFileInfo = new FileInfo(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "markdown-report.capella.combined.md")); + + Assert.That(() => this.markdownReportGenerator.GenerateCombinedReport(inputDirectory, reportFileInfo), Throws.Nothing); + + reportFileInfo.Refresh(); + Assert.That(reportFileInfo.Exists, Is.True); + + var markdown = File.ReadAllText(reportFileInfo.FullName); + + Assert.Multiple(() => + { + // a single document must contain classifiers that originate from several different .ecore files + Assert.That(markdown, Does.Contain("CapellaElement")); // CapellaCore.ecore + Assert.That(markdown, Does.Contain("LogicalArchitecture")); // LogicalArchitecture.ecore + Assert.That(markdown, Does.Contain("SystemAnalysis")); // ContextArchitecture.ecore + Assert.That(markdown, Does.Contain("PhysicalArchitecture")); // PhysicalArchitecture.ecore + Assert.That(markdown, Does.Contain("OperationalAnalysis")); // OperationalAnalysis.ecore + }); + } + + [Test] + public void Verify_that_a_combined_markdown_report_from_an_entry_file_includes_reachable_models() + { + var modelFileInfo = new FileInfo(Path.Combine(CapellaDirectory, "LogicalArchitecture.ecore")); + + var markdown = this.markdownReportGenerator.GenerateCombinedReport(modelFileInfo); + + Assert.Multiple(() => + { + Assert.That(markdown, Does.Contain("LogicalArchitecture")); + Assert.That(markdown, Does.Contain("ComponentArchitecture")); + }); + } + + /// + /// Enumerates the Capella .ecore file names available in the test output directory. + /// + private static IEnumerable CapellaModelFiles() + { + return Directory.EnumerateFiles(CapellaDirectory, "*.ecore").Select(file => Path.GetFileName(file)!); + } + } +} diff --git a/ECoreNetto.Reporting.Tests/Generators/CapellaXlReportGeneratorTestFixture.cs b/ECoreNetto.Reporting.Tests/Generators/CapellaXlReportGeneratorTestFixture.cs new file mode 100644 index 0000000..6d61d75 --- /dev/null +++ b/ECoreNetto.Reporting.Tests/Generators/CapellaXlReportGeneratorTestFixture.cs @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------------------------------ +// +// +// Copyright 2017-2026 Starion Group S.A. +// SPDX-License-Identifier: Apache-2.0 +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ECoreNetto.Reporting.Tests.Generators +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + + using ClosedXML.Excel; + + using ECoreNetto.Reporting.Generators; + + using NUnit.Framework; + + /// + /// Suite of tests that verify the produces an Excel report for the Eclipse + /// Capella metamodel (21 cross-referencing .ecore files), both per file and as a single combined + /// workbook of the whole metamodel. + /// + [TestFixture] + public class CapellaXlReportGeneratorTestFixture + { + private static readonly string CapellaDirectory = Path.Combine( + Path.GetDirectoryName(typeof(CapellaXlReportGeneratorTestFixture).Assembly.Location)!, + "Data", + "capella"); + + private XlReportGenerator generator = null!; + + [SetUp] + public void SetUp() + { + this.generator = new XlReportGenerator(); + } + + [Test] + [TestCaseSource(nameof(CapellaModelFiles))] + public void Verify_that_an_excel_report_is_generated_for_a_capella_model(string fileName) + { + var modelFileInfo = new FileInfo(Path.Combine(CapellaDirectory, fileName)); + + var reportFileInfo = new FileInfo(Path.Combine( + TestContext.CurrentContext.TestDirectory, + $"xl-report.capella.{Path.GetFileNameWithoutExtension(fileName)}.xlsx")); + + Assert.That(() => this.generator.GenerateReport(modelFileInfo, reportFileInfo), Throws.Nothing); + + reportFileInfo.Refresh(); + Assert.That(reportFileInfo.Exists, Is.True); + } + + [Test] + public void Verify_that_a_single_combined_excel_report_is_generated_for_the_whole_capella_metamodel() + { + var inputDirectory = new DirectoryInfo(CapellaDirectory); + + var reportFileInfo = new FileInfo(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "xl-report.capella.combined.xlsx")); + + Assert.That(() => this.generator.GenerateCombinedReport(inputDirectory, reportFileInfo), Throws.Nothing); + + reportFileInfo.Refresh(); + Assert.That(reportFileInfo.Exists, Is.True); + + using var workbook = new XLWorkbook(reportFileInfo.FullName); + + var info = workbook.Worksheet("Model Info"); + var eClassSheet = workbook.Worksheet("EClass"); + + Assert.Multiple(() => + { + // the info sheet lists all the included root packages + Assert.That(info.Cell(6, 1).GetString(), Is.EqualTo("Included Packages")); + + // a single workbook must list classes that originate from several different .ecore files + Assert.That(eClassSheet.CellsUsed().Any(c => c.GetString() == "CapellaElement"), Is.True); + Assert.That(eClassSheet.CellsUsed().Any(c => c.GetString() == "LogicalArchitecture"), Is.True); + Assert.That(eClassSheet.CellsUsed().Any(c => c.GetString() == "SystemAnalysis"), Is.True); + Assert.That(eClassSheet.CellsUsed().Any(c => c.GetString() == "PhysicalArchitecture"), Is.True); + }); + } + + [Test] + public void Verify_that_a_combined_excel_report_from_an_entry_file_includes_reachable_models() + { + var modelFileInfo = new FileInfo(Path.Combine(CapellaDirectory, "LogicalArchitecture.ecore")); + var reportFileInfo = new FileInfo(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "xl-report.capella.reachable.xlsx")); + + Assert.That(() => this.generator.GenerateCombinedReport(modelFileInfo, reportFileInfo), Throws.Nothing); + + reportFileInfo.Refresh(); + Assert.That(reportFileInfo.Exists, Is.True); + + using var workbook = new XLWorkbook(reportFileInfo.FullName); + var eClassSheet = workbook.Worksheet("EClass"); + + Assert.Multiple(() => + { + Assert.That(eClassSheet.CellsUsed().Any(c => c.GetString() == "LogicalArchitecture"), Is.True); + Assert.That(eClassSheet.CellsUsed().Any(c => c.GetString() == "ComponentArchitecture"), Is.True); + }); + } + + /// + /// Enumerates the Capella .ecore file names available in the test output directory. + /// + private static IEnumerable CapellaModelFiles() + { + return Directory.EnumerateFiles(CapellaDirectory, "*.ecore").Select(file => Path.GetFileName(file)!); + } + } +} diff --git a/ECoreNetto.Reporting.Tests/Generators/HtmlReportGeneratorTestFixture.cs b/ECoreNetto.Reporting.Tests/Generators/HtmlReportGeneratorTestFixture.cs index 116517c..ccb86d3 100644 --- a/ECoreNetto.Reporting.Tests/Generators/HtmlReportGeneratorTestFixture.cs +++ b/ECoreNetto.Reporting.Tests/Generators/HtmlReportGeneratorTestFixture.cs @@ -195,5 +195,24 @@ public void Verify_That_when_outputpath_is_null_exception_is_thrown() { Assert.That(() => this.htmlReportGenerator.IsValidReportExtension(null!), Throws.ArgumentNullException); } + + [Test] + public void Verify_that_combined_report_methods_throw_when_arguments_are_null() + { + FileInfo? modelFileInfo = null; + DirectoryInfo? directory = null; + var validModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); + var validOutput = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "html-report.combined-null.html")); + + Assert.Multiple(() => + { + Assert.That(() => this.htmlReportGenerator.GenerateCombinedReport(modelFileInfo!), Throws.ArgumentNullException); + Assert.That(() => this.htmlReportGenerator.GenerateCombinedReport(directory!), Throws.ArgumentNullException); + Assert.That(() => this.htmlReportGenerator.GenerateCombinedReport(modelFileInfo!, validOutput), Throws.ArgumentNullException); + Assert.That(() => this.htmlReportGenerator.GenerateCombinedReport(validModel, (FileInfo)null!), Throws.ArgumentNullException); + Assert.That(() => this.htmlReportGenerator.GenerateCombinedReport(directory!, validOutput), Throws.ArgumentNullException); + Assert.That(() => this.htmlReportGenerator.GenerateCombinedReport(new DirectoryInfo(TestContext.CurrentContext.TestDirectory), (FileInfo)null!), Throws.ArgumentNullException); + }); + } } } diff --git a/ECoreNetto.Reporting.Tests/Generators/MarkdownReportGeneratorTestFixture.cs b/ECoreNetto.Reporting.Tests/Generators/MarkdownReportGeneratorTestFixture.cs index 469cf2f..f1884e1 100644 --- a/ECoreNetto.Reporting.Tests/Generators/MarkdownReportGeneratorTestFixture.cs +++ b/ECoreNetto.Reporting.Tests/Generators/MarkdownReportGeneratorTestFixture.cs @@ -62,6 +62,22 @@ public void Verify_that_the_report_generator_generates_a_report(string model) Assert.That(() => this.markdownReportGenerator.GenerateReport(modelFileInfo, reportFileInfo), Throws.Nothing); } + [Test] + public void Verify_that_combined_markdown_report_methods_throw_when_arguments_are_null() + { + var validModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); + var validOutput = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "markdown-report.null.md")); + + Assert.Multiple(() => + { + Assert.That(() => this.markdownReportGenerator.GenerateCombinedReport((FileInfo)null!), Throws.ArgumentNullException); + Assert.That(() => this.markdownReportGenerator.GenerateCombinedReport((DirectoryInfo)null!), Throws.ArgumentNullException); + Assert.That(() => this.markdownReportGenerator.GenerateCombinedReport((FileInfo)null!, validOutput), Throws.ArgumentNullException); + Assert.That(() => this.markdownReportGenerator.GenerateCombinedReport(validModel, null!), Throws.ArgumentNullException); + Assert.That(() => this.markdownReportGenerator.GenerateCombinedReport(new DirectoryInfo(TestContext.CurrentContext.TestDirectory), null!), Throws.ArgumentNullException); + }); + } + [Test] public void Verify_that_the_generated_markdown_contains_the_expected_content() { diff --git a/ECoreNetto.Reporting.Tests/Generators/ModelInspectorTestFixture.cs b/ECoreNetto.Reporting.Tests/Generators/ModelInspectorTestFixture.cs index 738df18..31fadec 100644 --- a/ECoreNetto.Reporting.Tests/Generators/ModelInspectorTestFixture.cs +++ b/ECoreNetto.Reporting.Tests/Generators/ModelInspectorTestFixture.cs @@ -125,6 +125,29 @@ public void Verify_that_the_report_generator_generates_a_report() Assert.That(() => this.modelInspector.GenerateReport(modelFileInfo, reportFileInfo), Throws.Nothing); } + [Test] + public void Verify_that_a_combined_inspection_report_is_generated_for_a_multi_file_metamodel() + { + var capellaEntry = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "capella", "LogicalArchitecture.ecore")); + var output = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "inspection.capella.combined.txt")); + + Assert.That(() => this.modelInspector.GenerateCombinedReport(capellaEntry, output), Throws.Nothing); + + output.Refresh(); + Assert.That(output.Exists, Is.True); + } + + [Test] + public void Verify_that_GenerateCombinedReport_throws_when_arguments_are_null() + { + Assert.Multiple(() => + { + Assert.That(() => this.modelInspector.GenerateCombinedReport((FileInfo)null!, reportFileInfo), Throws.ArgumentNullException); + Assert.That(() => this.modelInspector.GenerateCombinedReport(modelFileInfo, (FileInfo)null!), Throws.ArgumentNullException); + Assert.That(() => this.modelInspector.GenerateCombinedReport((DirectoryInfo)null!, reportFileInfo), Throws.ArgumentNullException); + }); + } + [Test] public void Verify_that_IsValidExcelReportExtension_returns_false_when_invalid() { diff --git a/ECoreNetto.Reporting.Tests/Generators/XlReportGeneratorTestFixture.cs b/ECoreNetto.Reporting.Tests/Generators/XlReportGeneratorTestFixture.cs index ca917e5..58d023f 100644 --- a/ECoreNetto.Reporting.Tests/Generators/XlReportGeneratorTestFixture.cs +++ b/ECoreNetto.Reporting.Tests/Generators/XlReportGeneratorTestFixture.cs @@ -83,6 +83,20 @@ public void Verify_that_the_class_enum_and_datatype_sheets_contain_the_expected_ }); } + [Test] + public void Verify_that_combined_excel_report_methods_throw_when_arguments_are_null() + { + var validDirectory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + + Assert.Multiple(() => + { + Assert.That(() => this.generator.GenerateCombinedReport((FileInfo)null!, this.reportFileInfo), Throws.ArgumentNullException); + Assert.That(() => this.generator.GenerateCombinedReport(this.modelFileInfo, (FileInfo)null!), Throws.ArgumentNullException); + Assert.That(() => this.generator.GenerateCombinedReport((DirectoryInfo)null!, this.reportFileInfo), Throws.ArgumentNullException); + Assert.That(() => this.generator.GenerateCombinedReport(validDirectory, null!), Throws.ArgumentNullException); + }); + } + [Test] public void Verify_that_IsValidReportExtension_returns_expected_results() { diff --git a/ECoreNetto.Reporting/Generators/HandleBarsReportGenerator.cs b/ECoreNetto.Reporting/Generators/HandleBarsReportGenerator.cs index f078a0b..8b891a6 100644 --- a/ECoreNetto.Reporting/Generators/HandleBarsReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/HandleBarsReportGenerator.cs @@ -103,7 +103,23 @@ protected void RegisterEmbeddedTemplate(string name) /// protected static HandlebarsPayload CreateHandlebarsPayload(EPackage rootPackage) { - var packages = rootPackage.QueryPackages(); + return CreateHandlebarsPayload(new[] { rootPackage }); + } + + /// + /// Creates a that aggregates the classifiers of all the provided root + /// s, so that a single report can document a multi-file metamodel. + /// + /// + /// the subject root s; the first is used as the primary package for the report + /// title and metadata. + /// + /// + /// an instance of + /// + protected static HandlebarsPayload CreateHandlebarsPayload(IReadOnlyList rootPackages) + { + var packages = rootPackages.SelectMany(rootPackage => rootPackage.QueryPackages()); var enums = new List(); var dataTypes = new List(); @@ -130,7 +146,7 @@ protected static HandlebarsPayload CreateHandlebarsPayload(EPackage rootPackage) var orderedClasses = eClasses.OrderBy(x => x.Name); var orderedInterfaces = eClasses.Where(x => x.Interface).OrderBy(x => x.Name); - var payload = new HandlebarsPayload(rootPackage, orderedEnums, orderedPrimitiveTypes, orderedDataTypes, orderedClasses, orderedInterfaces); + var payload = new HandlebarsPayload(rootPackages[0], orderedEnums, orderedPrimitiveTypes, orderedDataTypes, orderedClasses, orderedInterfaces); return payload; } diff --git a/ECoreNetto.Reporting/Generators/HtmlReportGenerator.cs b/ECoreNetto.Reporting/Generators/HtmlReportGenerator.cs index dd35988..b214407 100644 --- a/ECoreNetto.Reporting/Generators/HtmlReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/HtmlReportGenerator.cs @@ -103,15 +103,75 @@ public string GenerateReport(FileInfo modelPath, string customHtml = "") throw new ArgumentNullException(nameof(modelPath)); } + return this.GenerateReport(new[] { this.LoadRootPackage(modelPath) }, customHtml); + } + + /// + /// Generates a single HTML report that aggregates the entry Ecore model together with every + /// cross-referenced model that is reachable from it. + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// optional custom HTML that is injected into the report at the custom-HTML injection point. + /// + /// + /// the content of an HTML report in a string + /// + public string GenerateCombinedReport(FileInfo modelPath, string customHtml = "") + { + if (modelPath == null) + { + throw new ArgumentNullException(nameof(modelPath)); + } + + return this.GenerateReport(this.LoadRootPackages(modelPath), customHtml); + } + + /// + /// Generates a single HTML report that aggregates every .ecore model in the provided directory. + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// optional custom HTML that is injected into the report at the custom-HTML injection point. + /// + /// + /// the content of an HTML report in a string + /// + public string GenerateCombinedReport(DirectoryInfo inputDirectory, string customHtml = "") + { + if (inputDirectory == null) + { + throw new ArgumentNullException(nameof(inputDirectory)); + } + + return this.GenerateReport(this.LoadRootPackages(inputDirectory), customHtml); + } + + /// + /// Generates the HTML report for the provided root s. + /// + /// + /// the root s whose classifiers are documented in the report. + /// + /// + /// custom HTML that is injected into the report at the custom-HTML injection point. + /// + /// + /// the content of an HTML report in a string + /// + private string GenerateReport(IReadOnlyList rootPackages, string customHtml) + { var sw = Stopwatch.StartNew(); this.logger.LogInformation("Start Generating Html report tables"); var template = this.Templates["ecore-to-html-docs"]; - var rootPackage = this.LoadRootPackage(modelPath); - - var payload = CreateHandlebarsPayload(rootPackage); + var payload = CreateHandlebarsPayload(rootPackages); var inheritanceDiagramSvg = this.inheritanceDiagramRenderer.SvgRender(payload); @@ -188,9 +248,90 @@ public void GenerateReport(FileInfo modelPath, FileInfo outputPath, string custo throw new ArgumentNullException(nameof(outputPath)); } - var sw = Stopwatch.StartNew(); + this.WriteToFile(this.GenerateReport(modelPath, customHtml), outputPath); + } + + /// + /// Generates a single combined HTML report of the entry model and every cross-referenced model that is + /// reachable from it, and writes it to the provided . + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(FileInfo modelPath, FileInfo outputPath) + { + if (modelPath == null) + { + throw new ArgumentNullException(nameof(modelPath)); + } + + if (outputPath == null) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + this.WriteToFile(this.GenerateCombinedReport(modelPath, string.Empty), outputPath); + } + + /// + /// Generates a single combined HTML report of every .ecore model in the provided directory and + /// writes it to the provided . + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(DirectoryInfo inputDirectory, FileInfo outputPath) + { + this.GenerateCombinedReport(inputDirectory, outputPath, string.Empty); + } + + /// + /// Generates a single combined HTML report of every .ecore model in the provided directory and + /// writes it to the provided . + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + /// + /// custom HTML that is injected into the report at the custom-HTML injection point; pass + /// when none is required. + /// + public void GenerateCombinedReport(DirectoryInfo inputDirectory, FileInfo outputPath, string customHtml) + { + if (inputDirectory == null) + { + throw new ArgumentNullException(nameof(inputDirectory)); + } + + if (outputPath == null) + { + throw new ArgumentNullException(nameof(outputPath)); + } - var generatedHtml = this.GenerateReport(modelPath, customHtml); + this.WriteToFile(this.GenerateCombinedReport(inputDirectory, customHtml), outputPath); + } + + /// + /// Writes the generated HTML to the provided , overwriting an existing file. + /// + /// + /// the generated HTML content. + /// + /// + /// the path, including filename, where the output is to be written. + /// + private void WriteToFile(string generatedHtml, FileInfo outputPath) + { + var sw = Stopwatch.StartNew(); if (outputPath.Exists) { diff --git a/ECoreNetto.Reporting/Generators/IHtmlReportGenerator.cs b/ECoreNetto.Reporting/Generators/IHtmlReportGenerator.cs index 8e1cf52..6c6c068 100644 --- a/ECoreNetto.Reporting/Generators/IHtmlReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/IHtmlReportGenerator.cs @@ -45,5 +45,50 @@ public interface IHtmlReportGenerator : IReportGenerator /// when none is required. /// public void GenerateReport(FileInfo modelPath, FileInfo outputPath, string customHtml); + + /// + /// Generates a single combined HTML report of the entry model together with every cross-referenced + /// model that is reachable from it. + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// optional custom HTML that is injected into the report at the custom-HTML injection point. + /// + /// + /// the content of an HTML report in a string + /// + public string GenerateCombinedReport(FileInfo modelPath, string customHtml = ""); + + /// + /// Generates a single combined HTML report of every .ecore model in the provided directory. + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// optional custom HTML that is injected into the report at the custom-HTML injection point. + /// + /// + /// the content of an HTML report in a string + /// + public string GenerateCombinedReport(DirectoryInfo inputDirectory, string customHtml = ""); + + /// + /// Generates a single combined HTML report of every .ecore model in the provided directory and + /// writes it to the provided . + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + /// + /// custom HTML that is injected into the report at the custom-HTML injection point; pass + /// when none is required. + /// + public void GenerateCombinedReport(DirectoryInfo inputDirectory, FileInfo outputPath, string customHtml); } } diff --git a/ECoreNetto.Reporting/Generators/IMarkdownReportGenerator.cs b/ECoreNetto.Reporting/Generators/IMarkdownReportGenerator.cs index 57834bd..cfd8ec0 100644 --- a/ECoreNetto.Reporting/Generators/IMarkdownReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/IMarkdownReportGenerator.cs @@ -27,5 +27,28 @@ public interface IMarkdownReportGenerator : IReportGenerator /// the content of a Markdown report in a string /// public string GenerateReport(FileInfo modelPath); + + /// + /// Generates a single combined Markdown report of the entry model together with every cross-referenced + /// model that is reachable from it. + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// the content of a Markdown report in a string + /// + public string GenerateCombinedReport(FileInfo modelPath); + + /// + /// Generates a single combined Markdown report of every .ecore model in the provided directory. + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the content of a Markdown report in a string + /// + public string GenerateCombinedReport(DirectoryInfo inputDirectory); } } diff --git a/ECoreNetto.Reporting/Generators/IReportGenerator.cs b/ECoreNetto.Reporting/Generators/IReportGenerator.cs index a119927..334ccfa 100644 --- a/ECoreNetto.Reporting/Generators/IReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/IReportGenerator.cs @@ -29,6 +29,29 @@ public interface IReportGenerator /// public void GenerateReport(FileInfo modelPath, FileInfo outputPath); + /// + /// Generates a single combined report of the entry model together with every cross-referenced model + /// that is reachable from it. + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(FileInfo modelPath, FileInfo outputPath); + + /// + /// Generates a single combined report of every .ecore model in the provided directory. + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(DirectoryInfo inputDirectory, FileInfo outputPath); + /// /// Verifies whether the extension of the is valid or not /// diff --git a/ECoreNetto.Reporting/Generators/IXlReportGenerator.cs b/ECoreNetto.Reporting/Generators/IXlReportGenerator.cs index a1d9ccf..f7e31cd 100644 --- a/ECoreNetto.Reporting/Generators/IXlReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/IXlReportGenerator.cs @@ -11,7 +11,7 @@ namespace ECoreNetto.Reporting.Generators { /// /// The purpose of the is to generate reports of an - /// Ecore Model + /// Ecore Model. The combined (multi-file) report members are inherited from . /// public interface IXlReportGenerator : IReportGenerator { diff --git a/ECoreNetto.Reporting/Generators/MarkdownReportGenerator.cs b/ECoreNetto.Reporting/Generators/MarkdownReportGenerator.cs index 78d07df..c7eb022 100644 --- a/ECoreNetto.Reporting/Generators/MarkdownReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/MarkdownReportGenerator.cs @@ -10,6 +10,7 @@ namespace ECoreNetto.Reporting.Generators { using System; + using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -65,15 +66,66 @@ public string GenerateReport(FileInfo modelPath) throw new ArgumentNullException(nameof(modelPath)); } + return this.GenerateReport(new[] { this.LoadRootPackage(modelPath) }); + } + + /// + /// Generates a single combined Markdown report of the entry model together with every cross-referenced + /// model that is reachable from it. + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// the content of a Markdown report in a string + /// + public string GenerateCombinedReport(FileInfo modelPath) + { + if (modelPath == null) + { + throw new ArgumentNullException(nameof(modelPath)); + } + + return this.GenerateReport(this.LoadRootPackages(modelPath)); + } + + /// + /// Generates a single combined Markdown report of every .ecore model in the provided directory. + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the content of a Markdown report in a string + /// + public string GenerateCombinedReport(DirectoryInfo inputDirectory) + { + if (inputDirectory == null) + { + throw new ArgumentNullException(nameof(inputDirectory)); + } + + return this.GenerateReport(this.LoadRootPackages(inputDirectory)); + } + + /// + /// Generates the Markdown report for the provided root s. + /// + /// + /// the root s whose classifiers are documented in the report. + /// + /// + /// the content of a Markdown report in a string + /// + private string GenerateReport(IReadOnlyList rootPackages) + { var sw = Stopwatch.StartNew(); this.logger.LogInformation("Start Generating Markdown report"); var template = this.Templates["ecore-to-markdown-docs"]; - var rootPackage = this.LoadRootPackage(modelPath); - - var payload = CreateHandlebarsPayload(rootPackage); + var payload = CreateHandlebarsPayload(rootPackages); var generatedMarkdown = template(payload); @@ -103,9 +155,71 @@ public void GenerateReport(FileInfo modelPath, FileInfo outputPath) throw new ArgumentNullException(nameof(outputPath)); } - var sw = Stopwatch.StartNew(); + this.WriteToFile(this.GenerateReport(modelPath), outputPath); + } + + /// + /// Generates a single combined Markdown report of the entry model together with every cross-referenced + /// model that is reachable from it, and writes it to the provided . + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(FileInfo modelPath, FileInfo outputPath) + { + if (modelPath == null) + { + throw new ArgumentNullException(nameof(modelPath)); + } + + if (outputPath == null) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + this.WriteToFile(this.GenerateCombinedReport(modelPath), outputPath); + } + + /// + /// Generates a single combined Markdown report of every .ecore model in the provided directory + /// and writes it to the provided . + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(DirectoryInfo inputDirectory, FileInfo outputPath) + { + if (inputDirectory == null) + { + throw new ArgumentNullException(nameof(inputDirectory)); + } - var generatedMarkdown = this.GenerateReport(modelPath) ; + if (outputPath == null) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + this.WriteToFile(this.GenerateCombinedReport(inputDirectory), outputPath); + } + + /// + /// Writes the generated Markdown to the provided , overwriting an existing file. + /// + /// + /// the generated Markdown content. + /// + /// + /// the path, including filename, where the output is to be written. + /// + private void WriteToFile(string generatedMarkdown, FileInfo outputPath) + { + var sw = Stopwatch.StartNew(); if (outputPath.Exists) { @@ -117,7 +231,7 @@ public void GenerateReport(FileInfo modelPath, FileInfo outputPath) this.logger.LogInformation("Generated Markdown report in {ElapsedTime} [ms]", sw.ElapsedMilliseconds); } - + /// /// Verifies whether the extension of the is valid or not /// diff --git a/ECoreNetto.Reporting/Generators/ModelInspector.cs b/ECoreNetto.Reporting/Generators/ModelInspector.cs index edf75e6..7837e1e 100644 --- a/ECoreNetto.Reporting/Generators/ModelInspector.cs +++ b/ECoreNetto.Reporting/Generators/ModelInspector.cs @@ -475,7 +475,7 @@ public void GenerateReport(FileInfo modelPath, FileInfo outputPath) if (outputPath.Exists) { - outputPath.Delete(); + outputPath.Delete(); } using var writer = outputPath.CreateText(); @@ -484,6 +484,103 @@ public void GenerateReport(FileInfo modelPath, FileInfo outputPath) this.logger.LogInformation("Generated inspection report in {0} [ms]", sw.ElapsedMilliseconds); } + /// + /// Generates a single combined inspection report of the entry model together with every + /// cross-referenced model that is reachable from it. + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(FileInfo modelPath, FileInfo outputPath) + { + if (modelPath == null) + { + throw new ArgumentNullException(nameof(modelPath)); + } + + if (outputPath == null) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + var sw = Stopwatch.StartNew(); + + this.logger.LogInformation("Start Generating combined Inspection Report"); + + var rootPackages = this.LoadRootPackages(modelPath); + + var result = new StringBuilder(); + + result.Append(this.ReportHeader()); + + foreach (var rootPackage in rootPackages) + { + result.Append(this.Inspect(rootPackage, true)); + result.Append(this.AnalyzeDocumentation(rootPackage, true)); + } + + if (outputPath.Exists) + { + outputPath.Delete(); + } + + using var writer = outputPath.CreateText(); + writer.Write(result); + + this.logger.LogInformation("Generated combined inspection report in {0} [ms]", sw.ElapsedMilliseconds); + } + + /// + /// Generates a single combined inspection report of every .ecore model in the provided directory. + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(DirectoryInfo inputDirectory, FileInfo outputPath) + { + if (inputDirectory == null) + { + throw new ArgumentNullException(nameof(inputDirectory)); + } + + if (outputPath == null) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + var sw = Stopwatch.StartNew(); + + this.logger.LogInformation("Start Generating combined Inspection Report for directory {0}", inputDirectory.FullName); + + var rootPackages = this.LoadRootPackages(inputDirectory); + + var result = new StringBuilder(); + + result.Append(this.ReportHeader()); + + foreach (var rootPackage in rootPackages) + { + result.Append(this.Inspect(rootPackage, true)); + result.Append(this.AnalyzeDocumentation(rootPackage, true)); + } + + if (outputPath.Exists) + { + outputPath.Delete(); + } + + using var writer = outputPath.CreateText(); + writer.Write(result); + + this.logger.LogInformation("Generated combined inspection report in {0} [ms]", sw.ElapsedMilliseconds); + } + /// /// Generates the report header that is the first part of the text report /// diff --git a/ECoreNetto.Reporting/Generators/ReportGenerator.cs b/ECoreNetto.Reporting/Generators/ReportGenerator.cs index 979f309..9203525 100644 --- a/ECoreNetto.Reporting/Generators/ReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/ReportGenerator.cs @@ -9,7 +9,9 @@ namespace ECoreNetto.Reporting.Generators { + using System.Collections.Generic; using System.IO; + using System.Linq; using ECoreNetto.Resource; @@ -85,5 +87,84 @@ protected EPackage LoadRootPackage(FileInfo modelPath) return rootPackage; } + + /// + /// Loads the entry Ecore model together with every cross-referenced model that is demand-loaded while + /// resolving it, and returns the root of every resource in the resulting + /// . + /// + /// + /// the path to the entry Ecore model that is to be loaded. + /// + /// + /// A read-only list of the root s of the entry model and of every model that is + /// reachable from it through cross-references. The entry model's root package is first. + /// + protected IReadOnlyList LoadRootPackages(FileInfo modelPath) + { + if (modelPath == null) + { + throw new ArgumentNullException(nameof(modelPath)); + } + + this.logger.LogInformation("Loading Ecore model and referenced models from {0}", modelPath.FullName); + + var resourceSet = new ResourceSet(this.loggerFactory); + var uri = new System.Uri(modelPath.FullName); + var resource = resourceSet.CreateResource(uri); + resource.Load(null); + + return QueryRootPackages(resourceSet); + } + + /// + /// Loads every .ecore file in the provided into a single + /// and returns the root of every resource, so that a + /// single report can be produced for a multi-file metamodel. + /// + /// + /// the directory that contains the .ecore files that are to be loaded. + /// + /// + /// A read-only list of the root s of every loaded model. + /// + protected IReadOnlyList LoadRootPackages(DirectoryInfo inputDirectory) + { + if (inputDirectory == null) + { + throw new ArgumentNullException(nameof(inputDirectory)); + } + + this.logger.LogInformation("Loading all Ecore models from directory {0}", inputDirectory.FullName); + + var resourceSet = new ResourceSet(this.loggerFactory); + + foreach (var file in inputDirectory.EnumerateFiles("*.ecore")) + { + var uri = new System.Uri(file.FullName); + + // demand-load so files already pulled in through cross-references are not loaded twice + resourceSet.Resource(uri, true); + } + + return QueryRootPackages(resourceSet); + } + + /// + /// Queries the root of every resource contained in the provided + /// . + /// + /// + /// the whose resources' root packages are collected. + /// + /// + /// A read-only list of the root s, in resource order. + /// + private static IReadOnlyList QueryRootPackages(ResourceSet resourceSet) + { + return resourceSet.Resources + .SelectMany(resource => resource.Contents.OfType()) + .ToList(); + } } } diff --git a/ECoreNetto.Reporting/Generators/XlReportGenerator.cs b/ECoreNetto.Reporting/Generators/XlReportGenerator.cs index 840eae3..5c9cbc9 100644 --- a/ECoreNetto.Reporting/Generators/XlReportGenerator.cs +++ b/ECoreNetto.Reporting/Generators/XlReportGenerator.cs @@ -78,17 +78,80 @@ public void GenerateReport(FileInfo modelPath, FileInfo outputPath) throw new ArgumentNullException(nameof(outputPath)); } + this.GenerateReport(new[] { this.LoadRootPackage(modelPath) }, outputPath); + } + + /// + /// Generates a single combined Excel report of the entry model together with every cross-referenced + /// model that is reachable from it, and writes it to the provided . + /// + /// + /// the path to the entry Ecore model of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(FileInfo modelPath, FileInfo outputPath) + { + if (modelPath == null) + { + throw new ArgumentNullException(nameof(modelPath)); + } + + if (outputPath == null) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + this.GenerateReport(this.LoadRootPackages(modelPath), outputPath); + } + + /// + /// Generates a single combined Excel report of every .ecore model in the provided directory and + /// writes it to the provided . + /// + /// + /// the directory that contains the .ecore models of which the combined report is to be generated. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + public void GenerateCombinedReport(DirectoryInfo inputDirectory, FileInfo outputPath) + { + if (inputDirectory == null) + { + throw new ArgumentNullException(nameof(inputDirectory)); + } + + if (outputPath == null) + { + throw new ArgumentNullException(nameof(outputPath)); + } + + this.GenerateReport(this.LoadRootPackages(inputDirectory), outputPath); + } + + /// + /// Generates the Excel report for the provided root s and writes it to the + /// provided . + /// + /// + /// the root s whose classifiers are documented in the report. + /// + /// + /// the path, including filename, where the output is to be generated. + /// + private void GenerateReport(IReadOnlyList rootPackages, FileInfo outputPath) + { var sw = Stopwatch.StartNew(); this.logger.LogInformation("Start Generating Excel report tables"); - var rootPackage = this.LoadRootPackage(modelPath); - - var packages = rootPackage.QueryPackages(); + var packages = rootPackages.SelectMany(rootPackage => rootPackage.QueryPackages()).ToList(); using (var workbook = new XLWorkbook()) { - this.AddInfoSheet(workbook, rootPackage); + this.AddInfoSheet(workbook, rootPackages); this.AddClassSheet(workbook, packages); @@ -110,18 +173,21 @@ public void GenerateReport(FileInfo modelPath, FileInfo outputPath) /// /// The target to which the info worksheet is added /// - /// - /// The root + /// + /// The root s documented in the report; the first is treated as the primary + /// package and every included root package name is listed. /// - private void AddInfoSheet(XLWorkbook workbook, EPackage rootPackage) + private void AddInfoSheet(XLWorkbook workbook, IReadOnlyList rootPackages) { this.logger.LogDebug("Add info sheet"); + var rootPackage = rootPackages[0]; + var infoWorksheet = workbook.Worksheets.Add("Model Info"); infoWorksheet.Cell(1, 1).Value = "ECoreNetto.Reporting Version"; infoWorksheet.Cell(1, 2).Value = Assembly.GetExecutingAssembly().GetName().Version?.ToString(); - + infoWorksheet.Cell(2, 1).Value = "Generation Date"; infoWorksheet.Cell(2, 2).Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); @@ -134,6 +200,9 @@ private void AddInfoSheet(XLWorkbook workbook, EPackage rootPackage) infoWorksheet.Cell(5, 1).Value = "Root Package - ns uri"; infoWorksheet.Cell(5, 2).Value = rootPackage.NsUri; + infoWorksheet.Cell(6, 1).Value = "Included Packages"; + infoWorksheet.Cell(6, 2).Value = string.Join(", ", rootPackages.Select(package => package.Name)); + this.FormatSheet(infoWorksheet); } diff --git a/ECoreNetto.Tools.Tests/Commands/HtmlReportCommandTestFixture.cs b/ECoreNetto.Tools.Tests/Commands/HtmlReportCommandTestFixture.cs index 2f8ab65..231a71e 100644 --- a/ECoreNetto.Tools.Tests/Commands/HtmlReportCommandTestFixture.cs +++ b/ECoreNetto.Tools.Tests/Commands/HtmlReportCommandTestFixture.cs @@ -90,6 +90,68 @@ public async Task Verify_that_InvokeAsync_returns_0() Assert.That(result, Is.EqualTo(0), "InvokeAsync should return 0 upon success."); } + [Test] + public async Task Verify_that_InvokeAsync_generates_a_combined_report_when_include_referenced_models_is_set() + { + var args = new[] + { + "html-report", + "--no-logo", + "--include-referenced-models", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "html-report.combined.html") + }; + + var parseResult = this.rootCommand.Parse(args); + + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); + + this.htmlReportGenerator.Verify(x => x.GenerateCombinedReport(It.IsAny(), It.IsAny()), Times.Once); + this.htmlReportGenerator.Verify(x => x.GenerateReport(It.IsAny(), It.IsAny()), Times.Never); + + Assert.That(result, Is.EqualTo(0), "InvokeAsync should return 0 upon success."); + } + + [Test] + public async Task Verify_that_InvokeAsync_generates_a_combined_report_for_a_directory() + { + var args = new[] + { + "html-report", + "--no-logo", + "--input-directory", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "html-report.directory.html") + }; + + var parseResult = this.rootCommand.Parse(args); + + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); + + this.htmlReportGenerator.Verify(x => x.GenerateCombinedReport(It.IsAny(), It.IsAny()), Times.Once); + this.htmlReportGenerator.Verify(x => x.GenerateReport(It.IsAny(), It.IsAny()), Times.Never); + this.htmlReportGenerator.Verify(x => x.GenerateCombinedReport(It.IsAny(), It.IsAny()), Times.Never); + + Assert.That(result, Is.EqualTo(0), "InvokeAsync should return 0 upon success."); + } + + [Test] + public async Task Verify_that_when_the_input_directory_does_not_exist_returns_not_0() + { + var args = new[] + { + "html-report", + "--no-logo", + "--input-directory", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "does-not-exist"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "html-report.directory.html") + }; + + var parseResult = this.rootCommand.Parse(args); + + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); + + Assert.That(result, Is.EqualTo(-1), "InvokeAsync should return -1 upon failure."); + } + [Test] public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns_not_0() { diff --git a/ECoreNetto.Tools/Commands/ReportCommand.cs b/ECoreNetto.Tools/Commands/ReportCommand.cs index 665adef..7353f60 100644 --- a/ECoreNetto.Tools/Commands/ReportCommand.cs +++ b/ECoreNetto.Tools/Commands/ReportCommand.cs @@ -47,12 +47,22 @@ protected ReportCommand(string name, string? description = null) : base(name, de { Description = "The path to the ecore file", DefaultValueFactory = parseResult => new FileInfo("model.ecore"), - Required = true + Required = false }; inputModelFileOption.Aliases.Add("-i"); this.Add(inputModelFileOption); + var inputDirectoryOption = new Option(name: "--input-directory") + { + Description = "The path to a directory of .ecore files; produces a single combined report for every model in the directory", + DefaultValueFactory = parseResult => null, + Required = false + }; + + inputDirectoryOption.Aliases.Add("-d"); + this.Add(inputDirectoryOption); + var autoOpenReportOption = new Option(name: "--auto-open-report") { Description = "Open the generated report with its default application", @@ -62,6 +72,16 @@ protected ReportCommand(string name, string? description = null) : base(name, de autoOpenReportOption.Aliases.Add("-a"); this.Add(autoOpenReportOption); + + var includeReferencedModelsOption = new Option(name: "--include-referenced-models") + { + Description = "Produce a single combined report that also includes every cross-referenced .ecore model reachable from the input model", + DefaultValueFactory = parseResult => false, + Required = false + }; + + includeReferencedModelsOption.Aliases.Add("-r"); + this.Add(includeReferencedModelsOption); } } } diff --git a/ECoreNetto.Tools/Commands/ReportHandler.cs b/ECoreNetto.Tools/Commands/ReportHandler.cs index 078daa7..d58a41a 100644 --- a/ECoreNetto.Tools/Commands/ReportHandler.cs +++ b/ECoreNetto.Tools/Commands/ReportHandler.cs @@ -69,6 +69,12 @@ protected ReportHandler(IReportGenerator reportGenerator, IVersionChecker versio /// private FileInfo inputModel = null!; + /// + /// The optional of a directory of .ecore models; when set a single + /// combined report is generated for every model in the directory. + /// + private DirectoryInfo? inputDirectory; + /// /// The where the inspection report is to be generated /// @@ -80,6 +86,12 @@ protected ReportHandler(IReportGenerator reportGenerator, IVersionChecker versio /// private bool autoOpenReport; + /// + /// The value indicating whether the report should also include every cross-referenced model that is + /// reachable from the input model (a single combined report). + /// + private bool includeReferencedModels; + /// /// Asynchronously invokes the /// @@ -131,7 +143,18 @@ await AnsiConsole.Status() await Task.Delay(this.StatusDelay, cancellationToken); - this.ReportGenerator.GenerateReport(this.inputModel, this.outputReport); + if (this.inputDirectory != null) + { + this.ReportGenerator.GenerateCombinedReport(this.inputDirectory, this.outputReport); + } + else if (this.includeReferencedModels) + { + this.ReportGenerator.GenerateCombinedReport(this.inputModel, this.outputReport); + } + else + { + this.ReportGenerator.GenerateReport(this.inputModel, this.outputReport); + } AnsiConsole.MarkupLine( $"[grey]LOG:[/] Ecore {this.ReportGenerator.QueryReportType()} report generated at [bold]{this.outputReport.FullName}[/]"); @@ -181,6 +204,8 @@ private void ProcessParseResult(ParseResult parseResult) this.inputModel = parseResult.GetValue("--input-model")!; this.autoOpenReport = parseResult.GetValue("--auto-open-report"); this.outputReport = parseResult.GetValue("--output-report")!; + this.includeReferencedModels = parseResult.GetValue("--include-referenced-models"); + this.inputDirectory = parseResult.GetValue("--input-directory"); } /// @@ -196,6 +221,20 @@ protected bool InputValidation() AnsiConsole.Markup($"[blue]{ResourceLoader.QueryLogo()}[/]"); } + if (this.inputDirectory != null) + { + if (!this.inputDirectory.Exists) + { + AnsiConsole.WriteLine(""); + AnsiConsole.MarkupLine($"[red]The specified input directory does not exist[/]"); + AnsiConsole.MarkupLine($"[purple]{this.inputDirectory.FullName}[/]"); + AnsiConsole.WriteLine(""); + return false; + } + + return true; + } + if (!this.inputModel.Exists) { AnsiConsole.WriteLine("");