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("");