From a7f749aa90314dda5771c29540faf66c78f38240 Mon Sep 17 00:00:00 2001 From: samatstarion Date: Mon, 24 Nov 2025 16:43:06 +0100 Subject: [PATCH 1/2] [Refactor] and update to System.Commandline version 2.0.0 --- .../ECoreNetto.Extensions.Tests.csproj | 6 +- .../ECoreNetto.Extensions.csproj | 2 +- .../ECoreNetto.HandleBars.Tests.csproj | 6 +- .../ECoreNetto.HandleBars.csproj | 2 +- .../ECoreNetto.Reporting.Tests.csproj | 6 +- .../ECoreNetto.Reporting.csproj | 4 +- ECoreNetto.Tests/ECoreNetto.Tests.csproj | 6 +- .../Commands/HtmlReportCommandTestFixture.cs | 64 +++++-- .../MarkdownReportCommandTestFixture.cs | 62 +++++-- .../ModelInspectionCommandTestFixture.cs | 62 +++++-- ...xture.cs => XlReportCommandTestFixture.cs} | 70 +++++-- .../ECoreNetto.Tools.Tests.csproj | 6 +- ECoreNetto.Tools.Tests/ProgramTestFixture.cs | 59 ++++++ .../Services/VersionCheckerTestFixture.cs | 111 +++++++++-- .../Commands/HtmlReportCommand.cs | 27 ++- .../Commands/MarkdownReportCommand.cs | 29 +-- .../Commands/ModelInspectionCommand.cs | 29 +-- ECoreNetto.Tools/Commands/ReportCommand.cs | 59 +++--- ECoreNetto.Tools/Commands/ReportHandler.cs | 96 ++++++---- ECoreNetto.Tools/Commands/XlReportCommand.cs | 27 ++- ECoreNetto.Tools/ECoreNetto.Tools.csproj | 17 +- .../Middlewares/VersionCheckerMiddleware.cs | 91 --------- ECoreNetto.Tools/Program.cs | 175 ++++++++++++------ ECoreNetto.Tools/Resources/ascii-art.txt | 1 + ECoreNetto.Tools/Services/IVersionChecker.cs | 39 ++++ ECoreNetto.Tools/Services/VersionChecker.cs | 83 +++++++-- ECoreNetto/ECoreNetto.csproj | 4 +- EcoreNetto.sln.DotSettings | 2 +- 28 files changed, 763 insertions(+), 382 deletions(-) rename ECoreNetto.Tools.Tests/Commands/{ReportCommandTestFixture.cs => XlReportCommandTestFixture.cs} (56%) create mode 100644 ECoreNetto.Tools.Tests/ProgramTestFixture.cs delete mode 100644 ECoreNetto.Tools/Middlewares/VersionCheckerMiddleware.cs create mode 100644 ECoreNetto.Tools/Services/IVersionChecker.cs diff --git a/ECoreNetto.Extensions.Tests/ECoreNetto.Extensions.Tests.csproj b/ECoreNetto.Extensions.Tests/ECoreNetto.Extensions.Tests.csproj index cf49520..1c7f291 100644 --- a/ECoreNetto.Extensions.Tests/ECoreNetto.Extensions.Tests.csproj +++ b/ECoreNetto.Extensions.Tests/ECoreNetto.Extensions.Tests.csproj @@ -14,7 +14,7 @@ - + @@ -34,8 +34,8 @@ - - + + diff --git a/ECoreNetto.Extensions/ECoreNetto.Extensions.csproj b/ECoreNetto.Extensions/ECoreNetto.Extensions.csproj index fa2a831..09ed43f 100644 --- a/ECoreNetto.Extensions/ECoreNetto.Extensions.csproj +++ b/ECoreNetto.Extensions/ECoreNetto.Extensions.csproj @@ -38,7 +38,7 @@ - + diff --git a/ECoreNetto.HandleBars.Tests/ECoreNetto.HandleBars.Tests.csproj b/ECoreNetto.HandleBars.Tests/ECoreNetto.HandleBars.Tests.csproj index a4fdfc2..5ccd9a2 100644 --- a/ECoreNetto.HandleBars.Tests/ECoreNetto.HandleBars.Tests.csproj +++ b/ECoreNetto.HandleBars.Tests/ECoreNetto.HandleBars.Tests.csproj @@ -14,7 +14,7 @@ - + @@ -30,8 +30,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/ECoreNetto.HandleBars/ECoreNetto.HandleBars.csproj b/ECoreNetto.HandleBars/ECoreNetto.HandleBars.csproj index a47b55d..25f29c1 100644 --- a/ECoreNetto.HandleBars/ECoreNetto.HandleBars.csproj +++ b/ECoreNetto.HandleBars/ECoreNetto.HandleBars.csproj @@ -34,7 +34,7 @@ - + diff --git a/ECoreNetto.Reporting.Tests/ECoreNetto.Reporting.Tests.csproj b/ECoreNetto.Reporting.Tests/ECoreNetto.Reporting.Tests.csproj index 11eb0e6..c57f3a5 100644 --- a/ECoreNetto.Reporting.Tests/ECoreNetto.Reporting.Tests.csproj +++ b/ECoreNetto.Reporting.Tests/ECoreNetto.Reporting.Tests.csproj @@ -11,7 +11,7 @@ - + @@ -27,8 +27,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/ECoreNetto.Reporting/ECoreNetto.Reporting.csproj b/ECoreNetto.Reporting/ECoreNetto.Reporting.csproj index a0f9e56..4e7e2cc 100644 --- a/ECoreNetto.Reporting/ECoreNetto.Reporting.csproj +++ b/ECoreNetto.Reporting/ECoreNetto.Reporting.csproj @@ -53,12 +53,12 @@ - + - + diff --git a/ECoreNetto.Tests/ECoreNetto.Tests.csproj b/ECoreNetto.Tests/ECoreNetto.Tests.csproj index 648d921..9c9fb1e 100644 --- a/ECoreNetto.Tests/ECoreNetto.Tests.csproj +++ b/ECoreNetto.Tests/ECoreNetto.Tests.csproj @@ -14,7 +14,7 @@ - + @@ -34,8 +34,8 @@ - - + + diff --git a/ECoreNetto.Tools.Tests/Commands/HtmlReportCommandTestFixture.cs b/ECoreNetto.Tools.Tests/Commands/HtmlReportCommandTestFixture.cs index 50f8376..6fb4a9e 100644 --- a/ECoreNetto.Tools.Tests/Commands/HtmlReportCommandTestFixture.cs +++ b/ECoreNetto.Tools.Tests/Commands/HtmlReportCommandTestFixture.cs @@ -21,15 +21,16 @@ namespace ECoreNetto.Tools.Tests.Commands { using System; - using System.CommandLine.Invocation; + using System.CommandLine; using System.IO; + using System.Threading; using System.Threading.Tasks; using ECoreNetto.Reporting.Generators; using ECoreNetto.Tools.Commands; - - using Moq; + using ECoreNetto.Tools.Services; + using Moq; using NUnit.Framework; /// @@ -38,24 +39,36 @@ namespace ECoreNetto.Tools.Tests.Commands [TestFixture] public class HtmlReportCommandTestFixture { + private RootCommand rootCommand; + private Mock htmlReportGenerator; + private Mock versionChecker; + private HtmlReportCommand.Handler handler; + private CancellationTokenSource cts; + [SetUp] public void SetUp() { + this.cts = new CancellationTokenSource(); + + var htmlReportCommand = new HtmlReportCommand(); + this.rootCommand = new RootCommand(); + this.rootCommand.Add(htmlReportCommand); + this.htmlReportGenerator = new Mock(); + this.versionChecker = new Mock(); this.htmlReportGenerator.Setup(x => x.IsValidReportExtension(It.IsAny())) .Returns(new Tuple(true, "valid extension")); - this.handler = new HtmlReportCommand.Handler( - this.htmlReportGenerator.Object); + this.handler = new HtmlReportCommand.Handler(this.htmlReportGenerator.Object, this.versionChecker.Object); } [Test] - public void Verify_that_inspect_command_can_be_constructed() + public void Verify_that_HtmlReportCommand_can_be_constructed() { Assert.That(() => { @@ -66,27 +79,39 @@ public void Verify_that_inspect_command_can_be_constructed() [Test] public async Task Verify_that_InvokeAsync_returns_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "html-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "html-report.html") + }; - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); - this.handler.OutputReport = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "html-report.html")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); this.htmlReportGenerator.Verify(x => x.GenerateReport(It.IsAny(), It.IsAny()), Times.Once); + this.versionChecker.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Once); + Assert.That(result, Is.EqualTo(0), "InvokeAsync should return 0 upon success."); } [Test] public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns_not_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "html-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "non-existent.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "html-report.html") + }; - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "non-existent.ecore")); - this.handler.OutputReport = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "html-report.html")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); Assert.That(result, Is.EqualTo(-1), "InvokeAsync should return -1 upon failure."); } @@ -94,14 +119,19 @@ public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns [Test] public async Task Verify_that_when_the_output_extensions_is_not_supported_returns_not_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "html-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + }; this.htmlReportGenerator.Setup(x => x.IsValidReportExtension(It.IsAny())) .Returns(new Tuple(false, "invalid extension")); - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); Assert.That(result, Is.EqualTo(-1), "InvokeAsync should return -1 upon failure."); } diff --git a/ECoreNetto.Tools.Tests/Commands/MarkdownReportCommandTestFixture.cs b/ECoreNetto.Tools.Tests/Commands/MarkdownReportCommandTestFixture.cs index 6d82d47..83956ea 100644 --- a/ECoreNetto.Tools.Tests/Commands/MarkdownReportCommandTestFixture.cs +++ b/ECoreNetto.Tools.Tests/Commands/MarkdownReportCommandTestFixture.cs @@ -21,15 +21,16 @@ namespace ECoreNetto.Tools.Tests.Commands { using System; - using System.CommandLine.Invocation; + using System.CommandLine; using System.IO; + using System.Threading; using System.Threading.Tasks; using ECoreNetto.Reporting.Generators; using ECoreNetto.Tools.Commands; + using ECoreNetto.Tools.Services; using Moq; - using NUnit.Framework; /// @@ -38,24 +39,36 @@ namespace ECoreNetto.Tools.Tests.Commands [TestFixture] public class MarkdownReportCommandTestFixture { + private RootCommand rootCommand; + private Mock markdownReportGenerator; + private Mock versionChecker; + private MarkdownReportCommand.Handler handler; + private CancellationTokenSource cts; + [SetUp] public void SetUp() { + this.cts = new CancellationTokenSource(); + + var markdownReportCommand = new MarkdownReportCommand(); + this.rootCommand = new RootCommand(); + this.rootCommand.Add(markdownReportCommand); + this.markdownReportGenerator = new Mock(); + this.versionChecker = new Mock(); this.markdownReportGenerator.Setup(x => x.IsValidReportExtension(It.IsAny())) .Returns(new Tuple(true, "valid extension")); - this.handler = new MarkdownReportCommand.Handler( - this.markdownReportGenerator.Object); + this.handler = new MarkdownReportCommand.Handler(this.markdownReportGenerator.Object, this.versionChecker.Object); } [Test] - public void Verify_that_inspect_command_can_be_constructed() + public void Verify_that_MarkdownReportCommand_can_be_constructed() { Assert.That(() => { @@ -66,27 +79,39 @@ public void Verify_that_inspect_command_can_be_constructed() [Test] public async Task Verify_that_InvokeAsync_returns_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "md-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "md-report.md") + }; - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); - this.handler.OutputReport = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "md-report.md")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); this.markdownReportGenerator.Verify(x => x.GenerateReport(It.IsAny(), It.IsAny()), Times.Once); + this.versionChecker.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Once); + Assert.That(result, Is.EqualTo(0), "InvokeAsync should return 0 upon success."); } [Test] public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns_not_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "md-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "non-existent.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "md-report.md") + }; - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "non-existent.ecore")); - this.handler.OutputReport = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "md-report.md")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); Assert.That(result, Is.EqualTo(-1), "InvokeAsync should return -1 upon failure."); } @@ -94,14 +119,19 @@ public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns [Test] public async Task Verify_that_when_the_output_extensions_is_not_supported_returns_not_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "md-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + }; this.markdownReportGenerator.Setup(x => x.IsValidReportExtension(It.IsAny())) .Returns(new Tuple(false, "invalid extension")); - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); Assert.That(result, Is.EqualTo(-1), "InvokeAsync should return -1 upon failure."); } diff --git a/ECoreNetto.Tools.Tests/Commands/ModelInspectionCommandTestFixture.cs b/ECoreNetto.Tools.Tests/Commands/ModelInspectionCommandTestFixture.cs index 37a9d3b..7fedb6f 100644 --- a/ECoreNetto.Tools.Tests/Commands/ModelInspectionCommandTestFixture.cs +++ b/ECoreNetto.Tools.Tests/Commands/ModelInspectionCommandTestFixture.cs @@ -21,15 +21,16 @@ namespace ECoreNetto.Tools.Tests.Commands { using System; - using System.CommandLine.Invocation; + using System.CommandLine; using System.IO; + using System.Threading; using System.Threading.Tasks; using ECoreNetto.Reporting.Generators; using ECoreNetto.Tools.Commands; - - using Moq; + using ECoreNetto.Tools.Services; + using Moq; using NUnit.Framework; /// @@ -38,20 +39,32 @@ namespace ECoreNetto.Tools.Tests.Commands [TestFixture] public class ModelInspectionCommandTestFixture { + private RootCommand rootCommand; + private Mock modelInspector; + private Mock versionChecker; + private ModelInspectionCommand.Handler handler; + private CancellationTokenSource cts; + [SetUp] public void SetUp() { + this.cts = new CancellationTokenSource(); + + var modelInspectionCommand = new ModelInspectionCommand(); + this.rootCommand = new RootCommand(); + this.rootCommand.Add(modelInspectionCommand); + this.modelInspector = new Mock(); + this.versionChecker = new Mock(); this.modelInspector.Setup(x => x.IsValidReportExtension(It.IsAny())) .Returns(new Tuple(true, "valid extension")); - this.handler = new ModelInspectionCommand.Handler( - this.modelInspector.Object); + this.handler = new ModelInspectionCommand.Handler(this.modelInspector.Object, this.versionChecker.Object); } [Test] @@ -66,27 +79,39 @@ public void Verify_that_inspect_command_can_be_constructed() [Test] public async Task Verify_that_InvokeAsync_returns_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "inspect", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "inspection-report.txt") + }; - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); - this.handler.OutputReport = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "inspection-report.txt")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); this.modelInspector.Verify(x => x.GenerateReport(It.IsAny(), It.IsAny()), Times.Once); + this.versionChecker.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Once); + Assert.That(result, Is.EqualTo(0), "InvokeAsync should return 0 upon success."); } [Test] public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns_not_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "inspect", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "non-existent.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "inspection-report.txt") + }; - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "non-existent.ecore")); - this.handler.OutputReport = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "inspection-report.txt")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); Assert.That(result, Is.EqualTo(-1), "InvokeAsync should return -1 upon failure."); } @@ -94,14 +119,19 @@ public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns [Test] public async Task Verify_that_when_the_output_extensions_is_not_supported_returns_not_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "inspect", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + }; this.modelInspector.Setup(x => x.IsValidReportExtension(It.IsAny())) .Returns(new Tuple(false, "invalid extension")); - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); Assert.That(result, Is.EqualTo(-1), "InvokeAsync should return -1 upon failure."); } diff --git a/ECoreNetto.Tools.Tests/Commands/ReportCommandTestFixture.cs b/ECoreNetto.Tools.Tests/Commands/XlReportCommandTestFixture.cs similarity index 56% rename from ECoreNetto.Tools.Tests/Commands/ReportCommandTestFixture.cs rename to ECoreNetto.Tools.Tests/Commands/XlReportCommandTestFixture.cs index 19df855..0cdad36 100644 --- a/ECoreNetto.Tools.Tests/Commands/ReportCommandTestFixture.cs +++ b/ECoreNetto.Tools.Tests/Commands/XlReportCommandTestFixture.cs @@ -1,5 +1,5 @@ // ------------------------------------------------------------------------------------------------- -// +// // // Copyright 2017-2025 Starion Group S.A. // @@ -21,37 +21,50 @@ namespace ECoreNetto.Tools.Tests.Commands { using System; - using System.CommandLine.Invocation; + using System.CommandLine; using System.IO; + using System.Threading; using System.Threading.Tasks; using ECoreNetto.Reporting.Generators; using ECoreNetto.Tools.Commands; - - using Moq; + using ECoreNetto.Tools.Services; + using Moq; using NUnit.Framework; - + /// /// Suite of tests for the class. /// [TestFixture] - public class ReportCommandTestFixture + public class XlReportCommandTestFixture { + private RootCommand rootCommand; + private Mock reportGenerator; + private Mock versionChecker; + private XlReportCommand.Handler handler; + private CancellationTokenSource cts; + [SetUp] public void SetUp() { + this.cts = new CancellationTokenSource(); + + var xlReportCommand = new XlReportCommand(); + this.rootCommand = new RootCommand(); + this.rootCommand.Add(xlReportCommand); + this.reportGenerator = new Mock(); + this.versionChecker = new Mock(); this.reportGenerator.Setup(x => x.IsValidReportExtension(It.IsAny())) .Returns(new Tuple(true, "valid extension")); - this.handler = new XlReportCommand.Handler( - this.reportGenerator.Object); + this.handler = new XlReportCommand.Handler(this.reportGenerator.Object, this.versionChecker.Object); } [Test] @@ -66,27 +79,39 @@ public void Verify_that_report_command_can_be_constructed() [Test] public async Task Verify_that_InvokeAsync_returns_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "excel-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "tabular-report.xlsx") + }; - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); - this.handler.OutputReport = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "tabular-report.xlsx")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); this.reportGenerator.Verify(x => x.GenerateReport(It.IsAny(), It.IsAny()), Times.Once); + this.versionChecker.Verify(x => x.ExecuteAsync(It.IsAny()), Times.Once); + Assert.That(result, Is.EqualTo(0), "InvokeAsync should return 0 upon success."); } [Test] public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns_not_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "excel-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "non-existent.ecore"), + "--output-report", Path.Combine(TestContext.CurrentContext.TestDirectory, "tabular-report.xlsx") + }; - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "non-existent.ecore")); - this.handler.OutputReport = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "tabular-report.xlsx")); + var parseResult = this.rootCommand.Parse(args); - var result = await this.handler.InvokeAsync(invocationContext); + var result = await this.handler.InvokeAsync(parseResult, this.cts.Token); Assert.That(result, Is.EqualTo(-1), "InvokeAsync should return -1 upon failure."); } @@ -94,14 +119,19 @@ public async Task Verify_that_when_the_input_ecore_model_does_not_exists_returns [Test] public async Task Verify_that_when_the_output_extensions_is_not_supported_returns_not_0() { - var invocationContext = new InvocationContext(null!); + var args = new[] + { + "excel-report", + "--no-logo", + "--input-model", Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"), + }; this.reportGenerator.Setup(x => x.IsValidReportExtension(It.IsAny())) .Returns(new Tuple(false, "invalid extension")); - this.handler.InputModel = new FileInfo(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore")); - - var result = await this.handler.InvokeAsync(invocationContext); + 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."); } diff --git a/ECoreNetto.Tools.Tests/ECoreNetto.Tools.Tests.csproj b/ECoreNetto.Tools.Tests/ECoreNetto.Tools.Tests.csproj index 911e218..f73fffd 100644 --- a/ECoreNetto.Tools.Tests/ECoreNetto.Tools.Tests.csproj +++ b/ECoreNetto.Tools.Tests/ECoreNetto.Tools.Tests.csproj @@ -11,7 +11,7 @@ - + @@ -27,8 +27,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/ECoreNetto.Tools.Tests/ProgramTestFixture.cs b/ECoreNetto.Tools.Tests/ProgramTestFixture.cs new file mode 100644 index 0000000..b454994 --- /dev/null +++ b/ECoreNetto.Tools.Tests/ProgramTestFixture.cs @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2017-2025 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ECoreNetto.Tools.Tests +{ + using System.IO; + using System.Threading.Tasks; + using NUnit.Framework; + + using ECoreNetto.Tools; + + [TestFixture] + public class ProgramTestFixture + { + private string inputModel; + + [SetUp] + public void Setup() + { + this.inputModel = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"); + } + + [Test] + [TestCase("html-report")] + [TestCase("excel-report")] + [TestCase("inspect")] + public async Task Verify_that_console_app_can_generate_reports(string reportKind) + { + var args = new[] + { + reportKind, + "--no-logo", + "--log-level", "Warning", + "--input-model", this.inputModel, + }; + + var exitCode = await Program.Main(args); + + Assert.That(exitCode, Is.EqualTo(0)); + } + } +} diff --git a/ECoreNetto.Tools.Tests/Services/VersionCheckerTestFixture.cs b/ECoreNetto.Tools.Tests/Services/VersionCheckerTestFixture.cs index 18941b0..b2e9186 100644 --- a/ECoreNetto.Tools.Tests/Services/VersionCheckerTestFixture.cs +++ b/ECoreNetto.Tools.Tests/Services/VersionCheckerTestFixture.cs @@ -20,17 +20,14 @@ namespace ECoreNetto.Tools.Tests.Services { - using System; - using System.Net.Http; - using System.Threading.Tasks; - using ECoreNetto.Tools.Services; - using Microsoft.Extensions.Logging; - using NUnit.Framework; - using Serilog; + using System; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; [TestFixture] public class VersionCheckerTestFixture @@ -39,6 +36,10 @@ public class VersionCheckerTestFixture private ILoggerFactory? loggerFactory; + private TestHttpClientFactory httpClientFactory; + + private TestTimeOutHttpClientFactory timeOutHttpClientFactory; + [OneTimeSetUp] public void OneTimeSetUp() { @@ -51,28 +52,106 @@ public void OneTimeSetUp() { builder.AddSerilog(); }); + + this.httpClientFactory = new TestHttpClientFactory(); + this.timeOutHttpClientFactory = new TestTimeOutHttpClientFactory(); } [SetUp] public void SetUp() { - var httpClient = new HttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(5); + this.versionChecker = new VersionChecker(this.httpClientFactory, this.loggerFactory); + } + + [Test] + public async Task Verify_that_ExecuteAsync_does_not_throw() + { + var cts = new CancellationTokenSource(); + + await Assert.ThatAsync(() => this.versionChecker.ExecuteAsync(cts.Token), Throws.Nothing); + } + + [Test] + public async Task Verify_that_ExecuteAsync_does_not_throw_on_http_timeout() + { + var cts = new CancellationTokenSource(); - this.versionChecker = new VersionChecker(httpClient, this.loggerFactory); + var checker = new VersionChecker(this.timeOutHttpClientFactory, this.loggerFactory); + + await Assert.ThatAsync(() => checker.ExecuteAsync(cts.Token), Throws.Nothing); } [Test] - public async Task Verify_that_Query_version_returns_result() + public async Task Verify_that_when_cancelled_exception_is_thrown() + { + var cts = new CancellationTokenSource(); + + await cts.CancelAsync(); + + var checker = new VersionChecker(this.timeOutHttpClientFactory, this.loggerFactory); + + await Assert.ThatAsync(() => checker.ExecuteAsync(cts.Token), Throws.TypeOf()); + } + + /// + /// Very simple IHttpClientFactory used just for tests. + /// It always returns the HttpClient passed in the constructor. + /// + private sealed class TestHttpClientFactory : IHttpClientFactory + { + private readonly HttpClient client; + + public TestHttpClientFactory() + { + this.client = new HttpClient(new SuccessHandler()); + } + + public HttpClient CreateClient(string name) + { + return this.client; + } + } + + private class SuccessHandler : HttpMessageHandler { - var result = await this.versionChecker.QueryLatestReleaseAsync(); + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var json = "{\"tag_name\":\"1.2.3\",\"body\":\"notes\",\"html_url\":\"https://example.com\"}"; + return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(json) + }); + } + } - Assert.That(result, Is.Not.Null); + /// + /// Very simple IHttpClientFactory used just for tests. + /// It always returns a HttpClient. + /// + private sealed class TestTimeOutHttpClientFactory : IHttpClientFactory + { + private readonly HttpClient client; + public TestTimeOutHttpClientFactory() + { + this.client = new HttpClient(new TimeoutHandler()) { Timeout = TimeSpan.FromSeconds(1) }; + } - Log.Logger.Information(result.TagName); - Log.Logger.Information(result.Body); - Log.Logger.Information(result.HtmlUrl); + public HttpClient CreateClient(string name) + { + return this.client; + } + } + /// + /// Very simple IHttpClientFactory used just for tests. + /// It always returns a HttpClient that times out + /// + private class TimeoutHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + throw new TaskCanceledException(); + } } } } diff --git a/ECoreNetto.Tools/Commands/HtmlReportCommand.cs b/ECoreNetto.Tools/Commands/HtmlReportCommand.cs index 9a81632..0b3eea1 100644 --- a/ECoreNetto.Tools/Commands/HtmlReportCommand.cs +++ b/ECoreNetto.Tools/Commands/HtmlReportCommand.cs @@ -21,10 +21,10 @@ namespace ECoreNetto.Tools.Commands { using System.CommandLine; - using System.CommandLine.Invocation; using System.IO; using ECoreNetto.Reporting.Generators; + using ECoreNetto.Tools.Services; /// /// The that generates an HTML report @@ -36,19 +36,22 @@ public class HtmlReportCommand : ReportCommand /// public HtmlReportCommand() : base("html-report", "Generates a html report of the ECore model") { - var reportFileOption = new Option( - name: "--output-report", - description: "The path to the html report file. Supported extensions are '.html'", - getDefaultValue: () => new FileInfo("html-report.html")); - reportFileOption.AddAlias("-o"); - reportFileOption.IsRequired = true; - this.AddOption(reportFileOption); + var reportFileOption = new Option(name: "--output-report") + { + Description = "The path to the html report file. Supported extensions are '.html'", + DefaultValueFactory = parseResult => new FileInfo("html-report.html"), + Required = true + }; + + reportFileOption.Aliases.Add("-o"); + + this.Add(reportFileOption); } /// /// The Command Handler of the /// - public new class Handler : ReportHandler, ICommandHandler + public class Handler : ReportHandler { /// /// Initializes a nwe instance of the class. @@ -57,7 +60,11 @@ public HtmlReportCommand() : base("html-report", "Generates a html report of the /// The (injected) that is used to generate the /// excel report /// - public Handler(IHtmlReportGenerator htmlReportGenerator) : base(htmlReportGenerator) + /// + /// The used to check the github version + /// + public Handler(IHtmlReportGenerator htmlReportGenerator, IVersionChecker versionChecker) + : base(htmlReportGenerator, versionChecker) { } } diff --git a/ECoreNetto.Tools/Commands/MarkdownReportCommand.cs b/ECoreNetto.Tools/Commands/MarkdownReportCommand.cs index 5f69608..87ca937 100644 --- a/ECoreNetto.Tools/Commands/MarkdownReportCommand.cs +++ b/ECoreNetto.Tools/Commands/MarkdownReportCommand.cs @@ -21,11 +21,11 @@ namespace ECoreNetto.Tools.Commands { using System.CommandLine; - using System.CommandLine.Invocation; using System.IO; using ECoreNetto.Reporting.Generators; - + using ECoreNetto.Tools.Services; + /// /// The that generates a Markdown report /// @@ -36,19 +36,22 @@ public class MarkdownReportCommand : ReportCommand /// public MarkdownReportCommand() : base("md-report", "Generates a Markdown report of the ECore model") { - var reportFileOption = new Option( - name: "--output-report", - description: "The path to the markdown report file. Supported extensions are '.md'", - getDefaultValue: () => new FileInfo("md-report.md")); - reportFileOption.AddAlias("-o"); - reportFileOption.IsRequired = true; - this.AddOption(reportFileOption); + var reportFileOption = new Option(name: "--output-report") + { + Description = "The path to the markdown report file. Supported extensions are '.md'", + DefaultValueFactory = parseResult => new FileInfo("md-report.md"), + Required = true, + }; + + reportFileOption.Aliases.Add("-o"); + + this.Add(reportFileOption); } /// /// The Command Handler of the /// - public new class Handler : ReportHandler, ICommandHandler + public class Handler : ReportHandler { /// /// Initializes a nwe instance of the class. @@ -57,7 +60,11 @@ public MarkdownReportCommand() : base("md-report", "Generates a Markdown report /// The (injected) that is used to generate the /// excel report /// - public Handler(IMarkdownReportGenerator markdownReportGenerator) : base(markdownReportGenerator) + /// + /// The used to check the github version + /// + public Handler(IMarkdownReportGenerator markdownReportGenerator, IVersionChecker versionChecker) + : base(markdownReportGenerator, versionChecker) { } } diff --git a/ECoreNetto.Tools/Commands/ModelInspectionCommand.cs b/ECoreNetto.Tools/Commands/ModelInspectionCommand.cs index c7eda31..5bee980 100644 --- a/ECoreNetto.Tools/Commands/ModelInspectionCommand.cs +++ b/ECoreNetto.Tools/Commands/ModelInspectionCommand.cs @@ -21,11 +21,11 @@ namespace ECoreNetto.Tools.Commands { using System.CommandLine; - using System.CommandLine.Invocation; using System.IO; using ECoreNetto.Reporting.Generators; - + using ECoreNetto.Tools.Services; + /// /// The that inspects an ECore model and generates /// a text report @@ -37,19 +37,22 @@ public class ModelInspectionCommand : ReportCommand /// public ModelInspectionCommand() : base("inspect", "Inspects an ECore model and generates a text report") { - var reportFileOption = new Option( - name: "--output-report", - description: "The path to the text report file. Supported extensions is '.txt'", - getDefaultValue: () => new FileInfo("inspection-report.txt")); - reportFileOption.AddAlias("-o"); - reportFileOption.IsRequired = true; - this.AddOption(reportFileOption); + var reportFileOption = new Option(name: "--output-report") + { + Description = "The path to the text report file. Supported extensions is '.txt'", + DefaultValueFactory = parseResult => new FileInfo("inspection-report.txt"), + Required = true + }; + + reportFileOption.Aliases.Add("-o"); + + this.Add(reportFileOption); } /// /// The Command Handler of the /// - public new class Handler : ReportHandler, ICommandHandler + public class Handler : ReportHandler { /// /// Initializes a nwe instance of the class. @@ -58,7 +61,11 @@ public ModelInspectionCommand() : base("inspect", "Inspects an ECore model and g /// The (injected) that is used to generate the /// inspection report /// - public Handler(IModelInspector modelInspector) : base(modelInspector) + /// + /// The used to check the github version + /// + public Handler(IModelInspector modelInspector, IVersionChecker versionChecker) + : base(modelInspector, versionChecker) { } } diff --git a/ECoreNetto.Tools/Commands/ReportCommand.cs b/ECoreNetto.Tools/Commands/ReportCommand.cs index e7027cd..da83905 100644 --- a/ECoreNetto.Tools/Commands/ReportCommand.cs +++ b/ECoreNetto.Tools/Commands/ReportCommand.cs @@ -23,6 +23,8 @@ namespace ECoreNetto.Tools.Commands using System.CommandLine; using System.IO; + using Serilog.Events; + /// /// Abstract super class from which all report commands shall inherit /// @@ -35,27 +37,42 @@ public abstract class ReportCommand : Command /// The description of the command, shown in help. protected ReportCommand(string name, string description = null) : base(name, description) { - var noLogoOption = new Option( - name: "--no-logo", - description: "Suppress the logo", - getDefaultValue: () => false); - this.AddOption(noLogoOption); - - var inputModelFileOption = new Option( - name: "--input-model", - description: "The path to the ecore file", - getDefaultValue: () => new FileInfo("model.ecore")); - inputModelFileOption.AddAlias("-i"); - inputModelFileOption.IsRequired = true; - this.AddOption(inputModelFileOption); - - var autoOpenReportOption = new Option( - name: "--auto-open-report", - description: "Open the generated report with its default application", - getDefaultValue: () => false); - autoOpenReportOption.AddAlias("-a"); - autoOpenReportOption.IsRequired = false; - this.AddOption(autoOpenReportOption); + var noLogoOption = new Option(name: "--no-logo") + { + Description = "Suppress the logo", + DefaultValueFactory = parseResult => false, + }; + + this.Add(noLogoOption); + + var logLevelOption = new Option("--log-level") + { + Description = "Sets the logging level (Trace, Debug, Information, Warning, Error, Critical)", + Required = false, + DefaultValueFactory = parseResult => LogEventLevel.Information + }; + + this.Options.Add(logLevelOption); + + var inputModelFileOption = new Option(name: "--input-model") + { + Description = "The path to the ecore file", + DefaultValueFactory = parseResult => new FileInfo("model.ecore"), + Required = true + }; + + inputModelFileOption.Aliases.Add("-i"); + this.Add(inputModelFileOption); + + var autoOpenReportOption = new Option(name: "--auto-open-report") + { + Description = "Open the generated report with its default application", + DefaultValueFactory = parseResult => false, + Required = false + }; + + autoOpenReportOption.Aliases.Add("-a"); + this.Add(autoOpenReportOption); } } } diff --git a/ECoreNetto.Tools/Commands/ReportHandler.cs b/ECoreNetto.Tools/Commands/ReportHandler.cs index eef00ea..d9b4c03 100644 --- a/ECoreNetto.Tools/Commands/ReportHandler.cs +++ b/ECoreNetto.Tools/Commands/ReportHandler.cs @@ -21,19 +21,20 @@ namespace ECoreNetto.Tools.Commands { using System; - using System.CommandLine.Invocation; + using System.CommandLine; using System.Diagnostics; using System.IO; using System.Threading; + using System.Threading.Tasks; using ECoreNetto.Reporting.Generators; using ECoreNetto.Tools.Resources; + using ECoreNetto.Tools.Services; using Spectre.Console; - using System.Threading.Tasks; /// - /// Abstract super class from which all Report s need to derive + /// Abstract super class from which all Report s need to derive /// public abstract class ReportHandler { @@ -45,9 +46,13 @@ public abstract class ReportHandler /// /// The used to generate an ECore report /// - protected ReportHandler(IReportGenerator reportGenerator) + /// + /// The used to check the github version + /// + protected ReportHandler(IReportGenerator reportGenerator, IVersionChecker versionChecker) { this.ReportGenerator = reportGenerator ?? throw new ArgumentNullException(nameof(reportGenerator)); + this.VersionChecker = versionChecker; } /// @@ -56,62 +61,65 @@ protected ReportHandler(IReportGenerator reportGenerator) public IReportGenerator ReportGenerator { get; private set; } /// - /// Gets or sets the value indicating whether the logo should be shown or not + /// The used to check the github version /// - public bool NoLogo { get; set; } + public IVersionChecker VersionChecker { get; private set; } /// - /// Gets or sets the where the ecore model is located that is to be read + /// The value indicating whether the logo should be shown or not /// - public FileInfo InputModel { get; set; } + private bool noLogo; /// - /// Gets or sets the where the inspection report is to be generated + /// The where the ecore model is located that is to be read /// - public FileInfo OutputReport { get; set; } + private FileInfo inputModel; /// - /// Gets or sets the value indicating whether the generated report needs to be automatically be - /// opened once generated. + /// The where the inspection report is to be generated /// - public bool AutoOpenReport { get; set; } + private FileInfo outputReport; /// - /// Invokes the + /// The value indicating whether the generated report needs to be automatically be + /// opened once generated. /// - /// - /// The - /// - /// - /// 0 when successful, another if not - /// - public int Invoke(InvocationContext context) - { - throw new NotSupportedException("Please use InvokeAsync"); - } + private bool autoOpenReport; /// - /// Asynchronously invokes the + /// Asynchronously invokes the /// - /// - /// The + /// + /// The that carries the parsed command line arguments + /// + /// + /// The used to cancel the operation /// /// /// 0 when successful, another if not /// - public async Task InvokeAsync(InvocationContext context) + public async Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + await this.VersionChecker.ExecuteAsync(cancellationToken); + + this.ProcessParseResult(parseResult); + if (!this.InputValidation()) { return -1; } - var isValidExtension = this.ReportGenerator.IsValidReportExtension(this.OutputReport); + var isValidExtension = this.ReportGenerator.IsValidReportExtension(this.outputReport); if (!isValidExtension.Item1) { AnsiConsole.WriteLine(""); AnsiConsole.MarkupLine($"[red] {isValidExtension.Item2} [/]"); - AnsiConsole.MarkupLine($"[purple]{this.InputModel.FullName}[/]"); + AnsiConsole.MarkupLine($"[purple]{this.inputModel.FullName}[/]"); AnsiConsole.WriteLine(""); return -1; } @@ -129,10 +137,10 @@ await AnsiConsole.Status() Thread.Sleep(SleepTime); - this.ReportGenerator.GenerateReport(this.InputModel, this.OutputReport); + this.ReportGenerator.GenerateReport(this.inputModel, this.outputReport); AnsiConsole.MarkupLine( - $"[grey]LOG:[/] Ecore {this.ReportGenerator.QueryReportType()} report generated at [bold]{this.OutputReport.FullName}[/]"); + $"[grey]LOG:[/] Ecore {this.ReportGenerator.QueryReportType()} report generated at [bold]{this.outputReport.FullName}[/]"); Thread.Sleep(SleepTime); this.ExecuteAutoOpen(ctx); @@ -170,6 +178,20 @@ await AnsiConsole.Status() return 0; } + /// + /// Process the and set to the associated properties + /// + /// + /// The instance of that contains the parsed commandline arguments + /// + private void ProcessParseResult(ParseResult parseResult) + { + this.noLogo = parseResult.GetValue("--no-logo"); + this.inputModel = parseResult.GetValue("--input-model"); + this.autoOpenReport = parseResult.GetValue("--auto-open-report"); + this.outputReport = parseResult.GetValue("--output-report"); + } + /// /// validates the options /// @@ -178,16 +200,16 @@ await AnsiConsole.Status() /// protected bool InputValidation() { - if (!this.NoLogo) + if (!this.noLogo) { AnsiConsole.Markup($"[blue]{ResourceLoader.QueryLogo()}[/]"); } - if (!this.InputModel.Exists) + if (!this.inputModel.Exists) { AnsiConsole.WriteLine(""); AnsiConsole.MarkupLine($"[red]The specified input ecore model does not exist[/]"); - AnsiConsole.MarkupLine($"[purple]{this.InputModel.FullName}[/]"); + AnsiConsole.MarkupLine($"[purple]{this.inputModel.FullName}[/]"); AnsiConsole.WriteLine(""); return false; } @@ -203,14 +225,14 @@ protected bool InputValidation() /// protected void ExecuteAutoOpen(StatusContext ctx) { - if (this.AutoOpenReport) + if (this.autoOpenReport) { ctx.Status($"Opening generated report"); Thread.Sleep(SleepTime); try { - Process.Start(new ProcessStartInfo(this.OutputReport.FullName) + Process.Start(new ProcessStartInfo(this.outputReport.FullName) { UseShellExecute = true }); ctx.Status($"Generated report opened"); } diff --git a/ECoreNetto.Tools/Commands/XlReportCommand.cs b/ECoreNetto.Tools/Commands/XlReportCommand.cs index c8dbb19..d6b358e 100644 --- a/ECoreNetto.Tools/Commands/XlReportCommand.cs +++ b/ECoreNetto.Tools/Commands/XlReportCommand.cs @@ -21,10 +21,10 @@ namespace ECoreNetto.Tools.Commands { using System.CommandLine; - using System.CommandLine.Invocation; using System.IO; using ECoreNetto.Reporting.Generators; + using ECoreNetto.Tools.Services; /// /// The that generates Excel tabular report of @@ -37,19 +37,22 @@ public class XlReportCommand : ReportCommand /// public XlReportCommand() : base("excel-report", "Generates a tabular report of the ECore model") { - var reportFileOption = new Option( - name: "--output-report", - description: "The path to the tabular report file. Supported extensions are '.xlsx', '.xlsm', '.xltx' and '.xltm'", - getDefaultValue: () => new FileInfo("tabular-report.xlsx")); - reportFileOption.AddAlias("-o"); - reportFileOption.IsRequired = true; - this.AddOption(reportFileOption); + var reportFileOption = new Option(name: "--output-report") + { + Description = "The path to the tabular report file. Supported extensions are '.xlsx', '.xlsm', '.xltx' and '.xltm'", + DefaultValueFactory = parseResult => new FileInfo("tabular-report.xlsx"), + Required = true + }; + + reportFileOption.Aliases.Add("-o"); + + this.Add(reportFileOption); } /// /// The Command Handler of the /// - public new class Handler : ReportHandler, ICommandHandler + public class Handler : ReportHandler { /// /// Initializes a nwe instance of the class. @@ -58,7 +61,11 @@ public XlReportCommand() : base("excel-report", "Generates a tabular report of t /// The (injected) that is used to generate the /// excel report /// - public Handler(IXlReportGenerator xlReportGenerator) : base(xlReportGenerator) + /// + /// The used to check the github version + /// + public Handler(IXlReportGenerator xlReportGenerator, IVersionChecker versionChecker) + : base(xlReportGenerator, versionChecker) { } } diff --git a/ECoreNetto.Tools/ECoreNetto.Tools.csproj b/ECoreNetto.Tools/ECoreNetto.Tools.csproj index 110ab8d..5c05832 100644 --- a/ECoreNetto.Tools/ECoreNetto.Tools.csproj +++ b/ECoreNetto.Tools/ECoreNetto.Tools.csproj @@ -48,15 +48,14 @@ - - - - - - - - - + + + + + + + + diff --git a/ECoreNetto.Tools/Middlewares/VersionCheckerMiddleware.cs b/ECoreNetto.Tools/Middlewares/VersionCheckerMiddleware.cs deleted file mode 100644 index e58ec3c..0000000 --- a/ECoreNetto.Tools/Middlewares/VersionCheckerMiddleware.cs +++ /dev/null @@ -1,91 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// -// -// Copyright 2017-2025 Starion Group S.A. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// -// ------------------------------------------------------------------------------------------------ - -namespace ECoreNetto.Tools.Middlewares -{ - using System; - using System.CommandLine.Builder; - using System.CommandLine.Invocation; - using System.Net.Http; - using System.Reflection; - using System.Threading.Tasks; - - using ECoreNetto.Tools.Services; - - using Spectre.Console; - - /// - /// A middleware that checks whether a newer version is available - /// - internal static class VersionCheckerMiddleware - { - /// - /// Configures the application to show check for a new version - /// - /// - /// A command line builder. - /// - /// - /// The same instance of . - /// - public static CommandLineBuilder UseVersionChecker(this CommandLineBuilder builder) - { - return builder.AddMiddleware(async (context, next) => - { - var httpClient = new HttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(2); - var versionChecker = new VersionChecker(httpClient); - - try - { - var payload = await versionChecker.QueryLatestReleaseAsync(); - - if (payload != null) - { - var currentVersion = Assembly.GetExecutingAssembly().GetName().Version; - var publishedVersion = new Version(payload.TagName); - - if (currentVersion < publishedVersion) - { - AnsiConsole.WriteLine(""); - AnsiConsole.MarkupLine($"[Green] a newer version is available at {payload.HtmlUrl} [/]"); - AnsiConsole.MarkupLine($"[Green] {payload.Body.EscapeMarkup()} [/]"); - AnsiConsole.WriteLine(""); - } - else - { - AnsiConsole.WriteLine(""); - AnsiConsole.MarkupLine($"[Green] you are using the most recent version. [/]"); - AnsiConsole.WriteLine(""); - } - } - - await next(context); - } - catch (TaskCanceledException) - { - AnsiConsole.WriteLine(""); - AnsiConsole.MarkupLine($"[Red] Checking version at GitHub API timed out. [/]"); - AnsiConsole.WriteLine(""); - } - }, MiddlewareOrder.ExceptionHandler); - } - } -} diff --git a/ECoreNetto.Tools/Program.cs b/ECoreNetto.Tools/Program.cs index 5cf28c0..44cc264 100644 --- a/ECoreNetto.Tools/Program.cs +++ b/ECoreNetto.Tools/Program.cs @@ -21,25 +21,23 @@ namespace ECoreNetto.Tools { using System.CommandLine; - using System.CommandLine.Builder; - using System.CommandLine.Help; - using System.CommandLine.Hosting; - using System.CommandLine.Parsing; using System.Diagnostics.CodeAnalysis; - using System.Linq; + using System.Threading.Tasks; - using ECoreNetto.Reporting.Generators; - using ECoreNetto.Tools.Resources; - using ECoreNetto.Tools.Commands; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; - using Spectre.Console; + using Autofac.Extensions.DependencyInjection; + + using Serilog; + using Serilog.Core; + using Serilog.Events; + + using ECoreNetto.Tools.Commands; + using ECoreNetto.Reporting.Generators; - using Middlewares; + using ECoreNetto.Tools.Services; /// /// Main entry point for the command line application @@ -47,65 +45,71 @@ namespace ECoreNetto.Tools [ExcludeFromCodeCoverage] public static class Program { + /// + /// Runtime-adjustable Serilog minimum level. + /// + private static readonly LoggingLevelSwitch LoggingLevelSwitch = new(); + /// /// Main entry point for the command line application /// /// /// command line arguments /// - public static int Main(string[] args) + public static async Task Main(string[] args) { - var commandLineBuilder = BuildCommandLine() - .UseHost(_ => Host.CreateDefaultBuilder(args) - .ConfigureLogging(loggingBuilder => - loggingBuilder.AddFilter(level => - level == LogLevel.None)) - , builder => builder - .ConfigureServices((services) => - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - }) - .UseCommandHandler() - .UseCommandHandler() - .UseCommandHandler() - .UseCommandHandler() - ) - .UseDefaults() - - .Build(); - - return commandLineBuilder.Invoke(args); + using var host = CreateHostBuilder(args).Build(); + + var rootCommand = CreateCommandChain(host); + + var parseResult = rootCommand.Parse(args); + + var result = await parseResult.InvokeAsync(); + + return result; } /// - /// builds the root command + /// Creates the /// + /// + /// the command line arguments + /// /// - /// The with the root command set + /// a configured instance of /// - private static CommandLineBuilder BuildCommandLine() + private static IHostBuilder CreateHostBuilder(string[] args) { - var root = CreateCommandChain(); - - return new CommandLineBuilder(root) - .UseHelp(ctx => + var host = Host.CreateDefaultBuilder(args) + .ConfigureLogging(loggingBuilder => { - ctx.HelpBuilder.CustomizeLayout(_ => - HelpBuilder.Default - .GetLayout() - .Skip(1) // Skip the default command description section. - .Prepend( - _ => - { - AnsiConsole.Markup($"[blue]{ResourceLoader.QueryLogo()}[/]"); - } - )); + loggingBuilder.ClearProviders(); + var template = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}"; + var loggerConfig = new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.ControlledBy(LoggingLevelSwitch) + .WriteTo.File("uml4net.logs", + rollingInterval: RollingInterval.Day, + outputTemplate: template); + + var serilogLogger = loggerConfig.CreateLogger(); + loggingBuilder.AddSerilog(serilogLogger, dispose: true); + + loggingBuilder.SetMinimumLevel(LogLevel.Information); }) - .UseVersionChecker(); + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureServices(services => + { + services.AddHttpClient(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }); + + return host; } /// @@ -114,23 +118,72 @@ private static CommandLineBuilder BuildCommandLine() /// /// returns an instance of /// - private static RootCommand CreateCommandChain() + private static RootCommand CreateCommandChain(IHost host) { var root = new RootCommand("ECoreNetto Tools"); var reportCommand = new XlReportCommand(); - root.AddCommand(reportCommand); + reportCommand.SetAction((parseResult, cancellationToken) => + { + ApplyLogLevel(parseResult); + + using var scope = host.Services.CreateScope(); + var generator = scope.ServiceProvider.GetService(); + var versionChecker = scope.ServiceProvider.GetService(); + var handler = new XlReportCommand.Handler(generator, versionChecker); + return handler.InvokeAsync(parseResult, cancellationToken); + }); + root.Add(reportCommand); + var modelInspectionCommand = new ModelInspectionCommand(); - root.AddCommand(modelInspectionCommand); + modelInspectionCommand.SetAction((parseResult, cancellationToken) => + { + ApplyLogLevel(parseResult); + + using var scope = host.Services.CreateScope(); + var generator = scope.ServiceProvider.GetService(); + var versionChecker = scope.ServiceProvider.GetService(); + var handler = new ModelInspectionCommand.Handler(generator, versionChecker); + return handler.InvokeAsync(parseResult, cancellationToken); + }); + root.Add(modelInspectionCommand); var htmlReportCommand = new HtmlReportCommand(); - root.AddCommand(htmlReportCommand); + htmlReportCommand.SetAction((parseResult, cancellationToken) => + { + ApplyLogLevel(parseResult); + + using var scope = host.Services.CreateScope(); + var generator = scope.ServiceProvider.GetService(); + var versionChecker = scope.ServiceProvider.GetService(); + var handler = new HtmlReportCommand.Handler(generator, versionChecker); + return handler.InvokeAsync(parseResult, cancellationToken); + }); + root.Add(htmlReportCommand); var markdownReportCommand = new MarkdownReportCommand(); - root.AddCommand(markdownReportCommand); + markdownReportCommand.SetAction((parseResult, cancellationToken) => + { + ApplyLogLevel(parseResult); + + using var scope = host.Services.CreateScope(); + var generator = scope.ServiceProvider.GetService(); + var versionChecker = scope.ServiceProvider.GetService(); + var handler = new MarkdownReportCommand.Handler(generator, versionChecker); + return handler.InvokeAsync(parseResult, cancellationToken); + }); + root.Add(markdownReportCommand); return root; } + + /// + /// Reads the log level from the parse result and updates the Serilog >. + /// + private static void ApplyLogLevel(ParseResult parseResult) + { + LoggingLevelSwitch.MinimumLevel = parseResult.GetValue("--log-level"); + } } } diff --git a/ECoreNetto.Tools/Resources/ascii-art.txt b/ECoreNetto.Tools/Resources/ascii-art.txt index fe49c1e..7f10e81 100644 --- a/ECoreNetto.Tools/Resources/ascii-art.txt +++ b/ECoreNetto.Tools/Resources/ascii-art.txt @@ -8,5 +8,6 @@ copyright 2024 Starion Group S.A. https://www.stariongroup.eu email: info@stariongroup.eu + Website: https://ecorenetto.org ECORE TOOLS version: EcoreToolsVersion \ No newline at end of file diff --git a/ECoreNetto.Tools/Services/IVersionChecker.cs b/ECoreNetto.Tools/Services/IVersionChecker.cs new file mode 100644 index 0000000..0aa5693 --- /dev/null +++ b/ECoreNetto.Tools/Services/IVersionChecker.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2017-2025 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ECoreNetto.Tools.Services +{ + using System.Threading; + using System.Threading.Tasks; + + /// + /// A service that checks whether a newer version is available + /// + public interface IVersionChecker + { + /// + /// Checks for the lastest release + /// + /// + /// The used to cancel the operation + /// + Task ExecuteAsync(CancellationToken cancellationToken); + } +} diff --git a/ECoreNetto.Tools/Services/VersionChecker.cs b/ECoreNetto.Tools/Services/VersionChecker.cs index 018c647..ab2a30c 100644 --- a/ECoreNetto.Tools/Services/VersionChecker.cs +++ b/ECoreNetto.Tools/Services/VersionChecker.cs @@ -22,62 +22,117 @@ namespace ECoreNetto.Tools.Services { using System; using System.Net.Http; + using System.Reflection; using System.Text.Json; + using System.Threading; using System.Threading.Tasks; - + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; + using Spectre.Console; + /// /// The purpose of the is to check whether a newer version is available /// - public class VersionChecker + public class VersionChecker : IVersionChecker { /// - /// The (injected) used to check the latest version that is available + /// The used to log /// - private readonly HttpClient httpClient; + private readonly ILogger logger; /// - /// The used to log + /// The (injected) used to create an /// - private readonly ILogger logger; + private readonly IHttpClientFactory httpClientFactory; /// /// Initializes a new instance of the /// - /// - /// The (injected) used to check the latest version that is available + /// + /// The (injected) used to create an /// /// /// The (injected) used to set up logging /// - public VersionChecker(HttpClient httpClient, ILoggerFactory loggerFactory = null) + public VersionChecker(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory = null) { - this.httpClient = httpClient; + this.httpClientFactory = httpClientFactory; this.logger = loggerFactory == null ? NullLogger.Instance : loggerFactory.CreateLogger(); } + /// + /// Checks for the lastest release + /// + /// + /// The used to cancel the operation + /// + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + try + { + var payload = await QueryLatestReleaseAsync(cancellationToken); + + if (payload != null) + { + var currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + var publishedVersion = new Version(payload.TagName); + + if (currentVersion < publishedVersion) + { + AnsiConsole.WriteLine(""); + AnsiConsole.MarkupLine($"[Green] a newer version is available at {payload.HtmlUrl} [/]"); + AnsiConsole.MarkupLine($"[Green] {payload.Body.EscapeMarkup()} [/]"); + AnsiConsole.WriteLine(""); + } + else + { + AnsiConsole.WriteLine(""); + AnsiConsole.MarkupLine($"[Green] you are using the most recent version: {currentVersion}. [/]"); + AnsiConsole.WriteLine(""); + } + } + } + catch (TaskCanceledException) + { + AnsiConsole.WriteLine(""); + AnsiConsole.MarkupLine($"[Red] Checking version at GitHub API timed out. [/]"); + AnsiConsole.WriteLine(""); + } + } + /// /// Queries the latest version from the GitHub API /// + /// + /// The used to cancel the operation + /// /// /// an instance of or null if not found or a connection /// error occured /// - public async Task QueryLatestReleaseAsync() + public async Task QueryLatestReleaseAsync(CancellationToken cancellationToken) { + var httpClient = this.httpClientFactory.CreateClient(); + httpClient.Timeout = TimeSpan.FromSeconds(2); + const string requestUrl = "https://api.github.com/repos/STARIONGROUP/EcoreNetto/releases/latest"; try { - this.httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("ECoreNetto.Tools"); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("ECoreNetto.Tools"); - var response = await this.httpClient.GetAsync(requestUrl); + var response = await httpClient.GetAsync(requestUrl, cancellationToken); if (response.IsSuccessStatusCode) { - var jsonResponse = await response.Content.ReadAsStringAsync(); + var jsonResponse = await response.Content.ReadAsStringAsync(cancellationToken); var release = JsonSerializer.Deserialize(jsonResponse); return release; diff --git a/ECoreNetto/ECoreNetto.csproj b/ECoreNetto/ECoreNetto.csproj index 899b7fb..e375c74 100644 --- a/ECoreNetto/ECoreNetto.csproj +++ b/ECoreNetto/ECoreNetto.csproj @@ -41,8 +41,8 @@ - - + + diff --git a/EcoreNetto.sln.DotSettings b/EcoreNetto.sln.DotSettings index 5d55efc..f8b80d3 100644 --- a/EcoreNetto.sln.DotSettings +++ b/EcoreNetto.sln.DotSettings @@ -4,7 +4,7 @@ ------------------------------------------------------------------------------------------------- <copyright file="${File.FileName}" company="Starion Group S.A"> - Copyright 2017-2024 Starion Group S.A. + Copyright 2017-2025 Starion Group S.A. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From e4ca7cfe0066da4c4d0128dd38629e1d0db1df8a Mon Sep 17 00:00:00 2001 From: samatstarion Date: Mon, 24 Nov 2025 16:51:01 +0100 Subject: [PATCH 2/2] [Update] sonar.login to sonar.token --- .github/workflows/CodeQuality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CodeQuality.yml b/.github/workflows/CodeQuality.yml index 52b3478..44b8fce 100644 --- a/.github/workflows/CodeQuality.yml +++ b/.github/workflows/CodeQuality.yml @@ -45,4 +45,4 @@ jobs: - name: Sonarqube end env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: dotnet sonarscanner end /d:sonar.login="$SONAR_TOKEN" + run: dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN"