From 68aa93e56cd2eaafeef51c5cdf42750c90af0fdc Mon Sep 17 00:00:00 2001 From: samatstarion Date: Sun, 31 May 2026 19:29:33 +0200 Subject: [PATCH 1/3] [Improve] Code coverage --- .../BooleanHelperTestFixture.cs | 10 ++ .../DocumentationHelperTestFixture.cs | 95 ++++++++++++++ .../GeneralizationHelperTestFixture.cs | 34 +++++ .../StructuralFeatureHelperTestFixture.cs | 120 ++++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 ECoreNetto.HandleBars.Tests/DocumentationHelperTestFixture.cs diff --git a/ECoreNetto.HandleBars.Tests/BooleanHelperTestFixture.cs b/ECoreNetto.HandleBars.Tests/BooleanHelperTestFixture.cs index a0c43b8..8510dab 100644 --- a/ECoreNetto.HandleBars.Tests/BooleanHelperTestFixture.cs +++ b/ECoreNetto.HandleBars.Tests/BooleanHelperTestFixture.cs @@ -58,5 +58,15 @@ public void Verify_that_ToLowerCase_returns_expected_result() Assert.That(falseResult, Is.EqualTo("false")); } + + [Test] + public void Verify_that_ToLowerCase_throws_when_not_exactly_one_argument() + { + var template = "{{ #Boolean.ToLowerCase this that }}"; + + var action = this.handlebarsContenxt.Compile(template); + + Assert.Throws(() => action(true)); + } } } diff --git a/ECoreNetto.HandleBars.Tests/DocumentationHelperTestFixture.cs b/ECoreNetto.HandleBars.Tests/DocumentationHelperTestFixture.cs new file mode 100644 index 0000000..d82712b --- /dev/null +++ b/ECoreNetto.HandleBars.Tests/DocumentationHelperTestFixture.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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.HandleBars.Tests +{ + using System; + using System.Globalization; + using System.IO; + using System.Linq; + + using ECoreNetto; + + using HandlebarsDotNet; + + using NUnit.Framework; + + /// + /// Suite of tests for the class + /// + [TestFixture] + public class DocumentationHelperTestFixture + { + private IHandlebars handlebarsContenxt = null!; + + private EPackage root = null!; + + [SetUp] + public void SetUp() + { + this.handlebarsContenxt = Handlebars.Create(); + this.handlebarsContenxt.Configuration.FormatProvider = CultureInfo.InvariantCulture; + + DocumentationHelper.RegisteredDocumentationHelper(this.handlebarsContenxt); + + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore"); + + this.root = ModelLoader.Load(path); + } + + [Test] + public void Verify_that_RawDocumentation_returns_the_documentation_when_present() + { + var template = "{{ #RawDocumentation this }}"; + + var action = this.handlebarsContenxt.Compile(template); + + var eClass = this.root.EClassifiers.OfType().Single(x => x.Name == "Ingredient"); + + var result = action(eClass); + + Assert.That(result, Does.Contain("Any of the foods or substances")); + } + + [Test] + public void Verify_that_RawDocumentation_returns_placeholder_when_no_documentation_is_present() + { + var template = "{{ #RawDocumentation this }}"; + + var action = this.handlebarsContenxt.Compile(template); + + var eClass = this.root.EClassifiers.OfType().Single(x => x.Name == "Recipe"); + + var result = action(eClass); + + Assert.That(result, Is.EqualTo("No Documentation Provided")); + } + + [Test] + public void Verify_that_RawDocumentation_throws_when_context_is_not_an_EModelElement() + { + var template = "{{ #RawDocumentation this }}"; + + var action = this.handlebarsContenxt.Compile(template); + + Assert.Throws(() => action("not-an-emodelelement")); + } + } +} diff --git a/ECoreNetto.HandleBars.Tests/GeneralizationHelperTestFixture.cs b/ECoreNetto.HandleBars.Tests/GeneralizationHelperTestFixture.cs index 5b5cd2a..ee731e9 100644 --- a/ECoreNetto.HandleBars.Tests/GeneralizationHelperTestFixture.cs +++ b/ECoreNetto.HandleBars.Tests/GeneralizationHelperTestFixture.cs @@ -94,5 +94,39 @@ public void Verify_that_GeneralizationClasses_returns_interface_for_class_withou Assert.That(result, Is.EqualTo(": IRelation")); } + + [Test] + public void Verify_that_GeneralizationInterfaces_returns_empty_for_class_without_supertypes() + { + var template = "{{ #Generalization.Interfaces this }}"; + + var action = this.handlebarsContenxt.Compile(template); + + var eClass = this.root.EClassifiers.Single(x => x.Name == "Relation"); + + var result = action(eClass); + + Assert.That(result, Is.Empty); + } + + [Test] + public void Verify_that_GeneralizationInterfaces_throws_when_context_is_not_an_EClass() + { + var template = "{{ #Generalization.Interfaces this }}"; + + var action = this.handlebarsContenxt.Compile(template); + + Assert.That(() => action("not-an-eclass"), Throws.ArgumentException); + } + + [Test] + public void Verify_that_GeneralizationClasses_throws_when_context_is_not_an_EClass() + { + var template = "{{ #Generalization.Classes this }}"; + + var action = this.handlebarsContenxt.Compile(template); + + Assert.That(() => action("not-an-eclass"), Throws.ArgumentException); + } } } diff --git a/ECoreNetto.HandleBars.Tests/StructuralFeatureHelperTestFixture.cs b/ECoreNetto.HandleBars.Tests/StructuralFeatureHelperTestFixture.cs index caa84c5..02ea2d9 100644 --- a/ECoreNetto.HandleBars.Tests/StructuralFeatureHelperTestFixture.cs +++ b/ECoreNetto.HandleBars.Tests/StructuralFeatureHelperTestFixture.cs @@ -220,5 +220,125 @@ public void Verify_that_StructuralFeature_NameEqualsEnclosingType_throws_when_no Assert.Throws(() => action(new { feature = eStructuralFeature })); } + + [Test] + public void Verify_that_StructuralFeature_IsEnumerable_block_renders_only_for_enumerable_feature() + { + var template = "{{#StructuralFeature.IsEnumerable this}}ENUMERABLE{{/StructuralFeature.IsEnumerable}}"; + var action = this.handlebarsContenxt.Compile(template); + + var ingredients = this.root.EClassifiers.OfType().Single(x => x.Name == "Recipe") + .EStructuralFeatures.Single(x => x.Name == "ingredients"); + Assert.That(action(ingredients), Is.EqualTo("ENUMERABLE")); + + var minutes = this.root.EClassifiers.OfType().Single(x => x.Name == "TimeTrigger") + .EStructuralFeatures.Single(x => x.Name == "minutes"); + Assert.That(action(minutes), Is.Empty); + } + + [Test] + public void Verify_that_StructuralFeature_IsAttribute_block_renders_only_for_attribute() + { + var template = "{{#StructuralFeature.IsAttribute this}}ATTRIBUTE{{/StructuralFeature.IsAttribute}}"; + var action = this.handlebarsContenxt.Compile(template); + + var minutes = this.root.EClassifiers.OfType().Single(x => x.Name == "TimeTrigger") + .EStructuralFeatures.Single(x => x.Name == "minutes"); + Assert.That(action(minutes), Is.EqualTo("ATTRIBUTE")); + + var ingredients = this.root.EClassifiers.OfType().Single(x => x.Name == "Recipe") + .EStructuralFeatures.Single(x => x.Name == "ingredients"); + Assert.That(action(ingredients), Is.Empty); + } + + [Test] + public void Verify_that_StructuralFeature_IsReference_block_renders_only_for_reference() + { + var template = "{{#StructuralFeature.IsReference this}}REFERENCE{{/StructuralFeature.IsReference}}"; + var action = this.handlebarsContenxt.Compile(template); + + var ingredients = this.root.EClassifiers.OfType().Single(x => x.Name == "Recipe") + .EStructuralFeatures.Single(x => x.Name == "ingredients"); + Assert.That(action(ingredients), Is.EqualTo("REFERENCE")); + + var minutes = this.root.EClassifiers.OfType().Single(x => x.Name == "TimeTrigger") + .EStructuralFeatures.Single(x => x.Name == "minutes"); + Assert.That(action(minutes), Is.Empty); + } + + [Test] + public void Verify_that_StructuralFeature_IsEnum_block_renders_only_for_enum_attribute() + { + var template = "{{#StructuralFeature.IsEnum this}}ENUM{{/StructuralFeature.IsEnum}}"; + var action = this.handlebarsContenxt.Compile(template); + + var unit = this.root.EClassifiers.OfType().Single(x => x.Name == "Amount") + .EStructuralFeatures.Single(x => x.Name == "unit"); + Assert.That(action(unit), Is.EqualTo("ENUM")); + + var ingredients = this.root.EClassifiers.OfType().Single(x => x.Name == "Recipe") + .EStructuralFeatures.Single(x => x.Name == "ingredients"); + Assert.That(action(ingredients), Is.Empty); + } + + [Test] + public void Verify_that_StructuralFeature_QueryStructuralFeatureNameEqualsEnclosingType_returns_expected_result() + { + var template = "{{ #StructuralFeature.QueryStructuralFeatureNameEqualsEnclosingType feature eClass }}"; + var action = this.handlebarsContenxt.Compile(template); + + var eClass = this.root.EClassifiers.OfType().Single(x => x.Name == "Amount"); + + var amount = eClass.EStructuralFeatures.Single(x => x.Name == "amount"); + Assert.That(action(new { feature = amount, eClass }), Is.EqualTo("True")); + + var unit = eClass.EStructuralFeatures.Single(x => x.Name == "unit"); + Assert.That(action(new { feature = unit, eClass }), Is.EqualTo("False")); + } + + [Test] + public void Verify_that_StructuralFeature_QueryTypeName_returns_expected_result() + { + var template = "{{ #StructuralFeature.QueryTypeName this }}"; + var action = this.handlebarsContenxt.Compile(template); + + var ingredients = this.root.EClassifiers.OfType().Single(x => x.Name == "Recipe") + .EStructuralFeatures.Single(x => x.Name == "ingredients"); + + Assert.That(action(ingredients), Is.EqualTo("Ingredient")); + } + + [Test] + public void Verify_that_StructuralFeature_QueryTypeName_throws_when_context_is_not_a_structural_feature() + { + var template = "{{ #StructuralFeature.QueryTypeName this }}"; + var action = this.handlebarsContenxt.Compile(template); + + Assert.That(() => action("not-a-structural-feature"), Throws.ArgumentException); + } + + [TestCase("StructuralFeature.QueryIsEnumerable", false)] + [TestCase("StructuralFeature.QueryIsAttribute", false)] + [TestCase("StructuralFeature.QueryIsReference", false)] + [TestCase("StructuralFeature.QueryIsEnum", false)] + [TestCase("StructuralFeature.QueryHasDefaultValue", false)] + [TestCase("StructuralFeature.QueryIsContainment", false)] + [TestCase("StructuralFeature.IsEnumerable", true)] + [TestCase("StructuralFeature.IsAttribute", true)] + [TestCase("StructuralFeature.IsReference", true)] + [TestCase("StructuralFeature.IsEnum", true)] + public void Verify_that_single_argument_helpers_throw_when_not_exactly_one_argument(string helper, bool isBlock) + { + var template = isBlock + ? "{{#" + helper + " this that}}X{{/" + helper + "}}" + : "{{ #" + helper + " this that }}"; + + var action = this.handlebarsContenxt.Compile(template); + + var ingredients = this.root.EClassifiers.OfType().Single(x => x.Name == "Recipe") + .EStructuralFeatures.Single(x => x.Name == "ingredients"); + + Assert.Throws(() => action(ingredients)); + } } } From 467072fa20581d350b9038aabb8368e45e8d8af7 Mon Sep 17 00:00:00 2001 From: samatstarion Date: Sun, 31 May 2026 19:41:03 +0200 Subject: [PATCH 2/3] [Improve] Code coverage --- .../ModelElementExtensionsTestFixture.cs | 11 +++++ .../StructuralFeatureExtensionsTestFixture.cs | 46 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/ECoreNetto.Extensions.Tests/ModelElementExtensionsTestFixture.cs b/ECoreNetto.Extensions.Tests/ModelElementExtensionsTestFixture.cs index 51a8099..5cffc61 100644 --- a/ECoreNetto.Extensions.Tests/ModelElementExtensionsTestFixture.cs +++ b/ECoreNetto.Extensions.Tests/ModelElementExtensionsTestFixture.cs @@ -132,5 +132,16 @@ public void Verify_that_RemoveUnwantedHtmlTags_returns_expected_results() var unchanged = ModelElementExtensions.RemoveUnwantedHtmlTags(html, new List()); Assert.That(unchanged, Is.EqualTo(html)); } + + [Test] + public void Verify_that_RemoveUnwantedHtmlTags_returns_input_when_there_are_no_element_or_text_nodes() + { + // a comment-only fragment yields no element/text nodes, so the input is returned unchanged + const string html = ""; + + var result = ModelElementExtensions.RemoveUnwantedHtmlTags(html, new List { "p" }); + + Assert.That(result, Is.EqualTo(html)); + } } } diff --git a/ECoreNetto.Extensions.Tests/StructuralFeatureExtensionsTestFixture.cs b/ECoreNetto.Extensions.Tests/StructuralFeatureExtensionsTestFixture.cs index 8a20e47..6563f4d 100644 --- a/ECoreNetto.Extensions.Tests/StructuralFeatureExtensionsTestFixture.cs +++ b/ECoreNetto.Extensions.Tests/StructuralFeatureExtensionsTestFixture.cs @@ -205,5 +205,51 @@ public void Verify_that_QueryIsNullable_returns_expected_results() var ingredientsFeature = recipeClass.EStructuralFeatures.Single(x => x.Name == "ingredients"); Assert.That(ingredientsFeature.QueryIsNullable(), Is.False); } + + [Test] + public void Verify_that_QueryStructuralFeatureNameEqualsEnclosingType_returns_expected_results() + { + var amountClass = this.rootPackage.EClassifiers.OfType().Single(x => x.Name == "Amount"); + + var amountFeature = amountClass.EStructuralFeatures.Single(x => x.Name == "amount"); + Assert.That(amountFeature.QueryStructuralFeatureNameEqualsEnclosingType(amountClass), Is.True); + + var unitFeature = amountClass.EStructuralFeatures.Single(x => x.Name == "unit"); + Assert.That(unitFeature.QueryStructuralFeatureNameEqualsEnclosingType(amountClass), Is.False); + } + + [Test] + public void Verify_that_QueryStructuralFeatureNameEqualsEnclosingType_throws_when_class_is_null() + { + var amountClass = this.rootPackage.EClassifiers.OfType().Single(x => x.Name == "Amount"); + var amountFeature = amountClass.EStructuralFeatures.Single(x => x.Name == "amount"); + + EClass nullClass = null!; + + Assert.That(() => amountFeature.QueryStructuralFeatureNameEqualsEnclosingType(nullClass), + Throws.ArgumentNullException); + } + + [Test] + public void Verify_that_the_extension_methods_throw_when_the_structural_feature_is_null() + { + EStructuralFeature feature = null!; + + var eClass = this.rootPackage.EClassifiers.OfType().First(); + + Assert.Multiple(() => + { + Assert.That(() => feature.QueryIsEnum(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryClass(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryIsEnumerable(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryIsAttribute(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryIsReference(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryIsContainment(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryHasDefaultValue(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryTypeName(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryIsNullable(), Throws.ArgumentNullException); + Assert.That(() => feature.QueryStructuralFeatureNameEqualsEnclosingType(eClass), Throws.ArgumentNullException); + }); + } } } From 5fb10867b7cbfdc77fc0624196ef51fac5c09655 Mon Sep 17 00:00:00 2001 From: samatstarion Date: Sun, 31 May 2026 20:00:35 +0200 Subject: [PATCH 3/3] [Fix] merge per-project coverage before summarizing so PR report shows real per-assembly coverage --- .github/workflows/CodeQuality.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CodeQuality.yml b/.github/workflows/CodeQuality.yml index 6234e09..d65bfb7 100644 --- a/.github/workflows/CodeQuality.yml +++ b/.github/workflows/CodeQuality.yml @@ -107,11 +107,23 @@ jobs: fail-on-error: false # Coverage comment: only once (ubuntu leg) and only on pull requests. + # The per-project test runs each emit their own coverage.cobertura.xml that also + # includes the shared production assemblies. Merge them into a single, de-duplicated + # report first so each assembly's coverage is the union across all suites (otherwise a + # shared assembly is counted once per file and its reported coverage is diluted). + - name: Merge coverage reports + if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' + uses: danielpalme/ReportGenerator-GitHub-Action@ee3806a36b8b2eb9594cb3e5fae045af7e5ead10 # v5.5.6 + with: + reports: TestResults/**/coverage.cobertura.xml + targetdir: CoverageReport + reporttypes: Cobertura + - name: Code coverage summary if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 with: - filename: TestResults/**/coverage.cobertura.xml + filename: CoverageReport/Cobertura.xml format: markdown output: both