Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Msagl" Version="1.1.6" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.Sbom.Targets" Version="4.1.5" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
Expand All @@ -28,6 +29,7 @@
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Spectre.Console" Version="0.57.0" />
<PackageVersion Include="Svg" Version="3.4.7" />
<PackageVersion Include="System.CommandLine" Version="2.0.9" />
<PackageVersion Include="System.IO.Packaging" Version="10.0.9" />
</ItemGroup>
Expand Down
63 changes: 63 additions & 0 deletions ECoreNetto.Extensions.Tests/AnchorExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// ------------------------------------------------------------------------------------------------
// <copyright file="AnchorExtensionsTestFixture.cs" company="Starion Group S.A.">
//
// Copyright 2017-2026 Starion Group S.A.
// SPDX-License-Identifier: Apache-2.0
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace ECoreNetto.Extensions.Tests
{
using System.IO;
using System.Linq;

using ECoreNetto.Extensions;
using ECoreNetto.Resource;

using NUnit.Framework;

/// <summary>
/// Suite of tests for the <see cref="AnchorExtensions"/> class
/// </summary>
[TestFixture]
public class AnchorExtensionsTestFixture
{
private EPackage rootPackage = null!;

[SetUp]
public void SetUp()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "recipe.ecore");
var uri = new System.Uri(Path.GetFullPath(path));

var resourceSet = new ResourceSet();
var resource = resourceSet.CreateResource(uri);

this.rootPackage = resource.Load(null);
}

[Test]
public void Verify_that_QueryAnchorId_returns_a_sanitized_slug()
{
var recipe = this.rootPackage.EClassifiers.OfType<EClass>().Single(x => x.Name == "Recipe");

var anchor = recipe.QueryAnchorId();

Assert.Multiple(() =>
{
// the slug contains only URL-safe characters and ends with the class name
Assert.That(anchor, Does.Match("^[A-Za-z0-9-]+$"));
Assert.That(anchor, Does.EndWith("Recipe"));
});
}

[Test]
public void Verify_that_QueryAnchorId_throws_when_argument_is_null()
{
EObject? eObject = null;

Assert.That(() => eObject!.QueryAnchorId(), Throws.ArgumentNullException);
}
}
}
128 changes: 128 additions & 0 deletions ECoreNetto.Extensions.Tests/ClassExtensionsFeaturesTestFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// ------------------------------------------------------------------------------------------------
// <copyright file="ClassExtensionsFeaturesTestFixture.cs" company="Starion Group S.A.">
//
// Copyright 2017-2026 Starion Group S.A.
// SPDX-License-Identifier: Apache-2.0
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace ECoreNetto.Extensions.Tests
{
using System.Collections.Generic;
using System.IO;
using System.Linq;

using ECoreNetto.Extensions;
using ECoreNetto.Resource;

using NUnit.Framework;

/// <summary>
/// Suite of tests for the report-parity extension methods on <see cref="ClassExtensions"/> that are
/// exercised against the feature-rich synthetic model.
/// </summary>
[TestFixture]
public class ClassExtensionsFeaturesTestFixture
{
private EPackage rootPackage = null!;
private List<EClass> allClasses = null!;

[SetUp]
public void SetUp()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "report-features.ecore");
var uri = new System.Uri(Path.GetFullPath(path));

var resourceSet = new ResourceSet();
var resource = resourceSet.CreateResource(uri);

this.rootPackage = resource.Load(null);
this.allClasses = this.rootPackage.EClassifiers.OfType<EClass>().ToList();
}

[Test]
public void Verify_that_QueryContainers_returns_the_containing_classes()
{
var address = this.allClasses.Single(x => x.Name == "Address");

var containers = address.QueryContainers(this.allClasses).ToList();

// Person owns a containment reference (addresses) whose type is Address
Assert.That(containers.Select(x => x.Name), Is.EquivalentTo(new[] { "Person" }));

Check warning on line 52 in ECoreNetto.Extensions.Tests/ClassExtensionsFeaturesTestFixture.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array

See more on https://sonarcloud.io/project/issues?id=STARIONGROUP_EcoreNetto&issues=AZ8z0VzKF1Kw6JMcC0jr&open=AZ8z0VzKF1Kw6JMcC0jr&pullRequest=112
}

[Test]
public void Verify_that_QueryContainers_returns_empty_when_no_containment_reference_targets_the_class()
{
var company = this.allClasses.Single(x => x.Name == "Company");

var containers = company.QueryContainers(this.allClasses);

Assert.That(containers, Is.Empty);
}

[Test]
public void Verify_that_QueryContainers_throws_when_argument_is_null()
{
EClass? @class = null;

Assert.That(() => @class!.QueryContainers(this.allClasses), Throws.ArgumentNullException);
}

[Test]
public void Verify_that_QueryAllDescendantSpecializations_returns_all_subclasses()
{
var describable = this.allClasses.Single(x => x.Name == "Describable");

var descendants = describable.QueryAllDescendantSpecializations(this.allClasses).ToList();

Assert.That(descendants.Select(x => x.Name), Is.EquivalentTo(new[] { "Person" }));

Check warning on line 80 in ECoreNetto.Extensions.Tests/ClassExtensionsFeaturesTestFixture.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array

See more on https://sonarcloud.io/project/issues?id=STARIONGROUP_EcoreNetto&issues=AZ8z0VzKF1Kw6JMcC0js&open=AZ8z0VzKF1Kw6JMcC0js&pullRequest=112
}

[Test]
public void Verify_that_QueryAllDescendantSpecializations_throws_when_argument_is_null()
{
EClass? @class = null;

Assert.That(() => @class!.QueryAllDescendantSpecializations(this.allClasses), Throws.ArgumentNullException);
}

[Test]
public void Verify_that_QueryConstraints_reads_the_constraint_and_its_ocl_body()
{
var person = this.allClasses.Single(x => x.Name == "Person");

var constraints = person.QueryConstraints().ToList();

Assert.That(constraints, Has.Count.EqualTo(1));

var constraint = constraints.Single();

Assert.Multiple(() =>
{
Assert.That(constraint.Name, Is.EqualTo("hasFullName"));
Assert.That(constraint.Language, Is.EqualTo("OCL"));
Assert.That(constraint.Body, Is.EqualTo("self.fullName.notEmpty()"));
});
}

[Test]
public void Verify_that_QueryConstraints_returns_empty_when_no_constraints_are_declared()
{
var address = this.allClasses.Single(x => x.Name == "Address");

var constraints = address.QueryConstraints();

Assert.That(constraints, Is.Empty);
}

[Test]
public void Verify_that_QueryConstraints_throws_when_argument_is_null()
{
EClass? @class = null;

Assert.That(() => @class!.QueryConstraints(), Throws.ArgumentNullException);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
<None Include="..\TestData\wizardEcore.ecore" Link="Data\wizardEcore.ecore">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="..\TestData\report-features.ecore" Link="Data\report-features.ecore">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
61 changes: 61 additions & 0 deletions ECoreNetto.Extensions/AnchorExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// ------------------------------------------------------------------------------------------------
// <copyright file="AnchorExtensions.cs" company="Starion Group S.A.">
//
// Copyright 2017-2026 Starion Group S.A.
// SPDX-License-Identifier: Apache-2.0
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace ECoreNetto.Extensions
{
using System;
using System.Text.RegularExpressions;

/// <summary>
/// Extension methods that produce stable, collision-free HTML anchors for <see cref="EObject"/>s.
/// </summary>
public static class AnchorExtensions
{
/// <summary>
/// The <see cref="Regex"/> that matches every run of characters that are not valid in an HTML
/// fragment identifier. A match timeout guards against pathological input.
/// </summary>
private static readonly Regex NonAnchorCharacters = new Regex("[^A-Za-z0-9]+", RegexOptions.None, TimeSpan.FromSeconds(1));

/// <summary>
/// Queries a stable, unique HTML anchor identifier for the provided <see cref="EObject"/>, derived
/// from its (unique) <see cref="EObject.Identifier"/> by replacing every run of characters that are
/// not valid in an HTML fragment with a single dash.
/// </summary>
/// <param name="eObject">
/// The <see cref="EObject"/> for which the anchor is computed.
/// </param>
/// <returns>
/// A sanitized anchor slug (e.g. <c>recipe-ecore-Recipe</c>), suitable for use as an <c>id</c>
/// attribute and as an <c>href="#..."</c> target.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="eObject"/> is null.
/// </exception>
public static string QueryAnchorId(this EObject eObject)
{
if (eObject == null)
{
throw new ArgumentNullException(nameof(eObject));
}

// A classifier that is referenced across resources (e.g. a super-type, an inherited feature's
// containing class or a feature type defined in another .ecore) may be an unattached proxy with
// no EContainer. Computing its Identifier would dereference a null EPackage, so fall back to the
// simple name for such detached classifiers.
var identifier = eObject is EClassifier { EContainer: null } detachedClassifier
? detachedClassifier.Name ?? string.Empty
: eObject.Identifier ?? string.Empty;

var sanitized = NonAnchorCharacters.Replace(identifier, "-").Trim('-');

return string.IsNullOrEmpty(sanitized) ? "anchor" : sanitized;
}
}
}
Loading
Loading