diff --git a/SysML2.NET.Tests/Extend/MetadataFeatureExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/MetadataFeatureExtensionsTestFixture.cs
index 6223faab..841f9527 100644
--- a/SysML2.NET.Tests/Extend/MetadataFeatureExtensionsTestFixture.cs
+++ b/SysML2.NET.Tests/Extend/MetadataFeatureExtensionsTestFixture.cs
@@ -1,38 +1,211 @@
-// -------------------------------------------------------------------------------------------------
+// -------------------------------------------------------------------------------------------------
//
-//
+//
// Copyright 2022-2026 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 SysML2.NET.Tests.Extend
{
using System;
-
+
using NUnit.Framework;
-
+
+ using SysML2.NET.Core.POCO.Core.Features;
+ using SysML2.NET.Core.POCO.Core.Types;
+ using SysML2.NET.Core.POCO.Kernel.FeatureValues;
+ using SysML2.NET.Core.POCO.Kernel.Functions;
using SysML2.NET.Core.POCO.Kernel.Metadata;
+ using SysML2.NET.Extensions;
[TestFixture]
public class MetadataFeatureExtensionsTestFixture
{
[Test]
- public void ComputeMetaclass_ThrowsNotSupportedException()
+ public void VerifyComputeMetaclass()
+ {
+ Assert.That(() => ((IMetadataFeature)null).ComputeMetaclass(), Throws.TypeOf());
+
+ var metadataFeature = new MetadataFeature();
+
+ // Empty: no FeatureTyping → null.
+ Assert.That(metadataFeature.ComputeMetaclass(), Is.Null);
+
+ // Negative: FeatureTyping pointing at a non-Metaclass Type → null.
+ var nonMetaclassType = new FeatureTyping { Type = new Feature() };
+ metadataFeature.AssignOwnership(nonMetaclassType);
+
+ Assert.That(metadataFeature.ComputeMetaclass(), Is.Null);
+
+ // Positive: FeatureTyping pointing at a Metaclass → returned.
+ var metaclass1 = new Metaclass();
+ var typingToMetaclass1 = new FeatureTyping { Type = metaclass1 };
+ metadataFeature.AssignOwnership(typingToMetaclass1);
+
+ Assert.That(metadataFeature.ComputeMetaclass(), Is.SameAs(metaclass1));
+
+ // Multiple Metaclass typings → first returned (insertion order).
+ var metaclass2 = new Metaclass();
+ var typingToMetaclass2 = new FeatureTyping { Type = metaclass2 };
+ metadataFeature.AssignOwnership(typingToMetaclass2);
+
+ Assert.That(metadataFeature.ComputeMetaclass(), Is.SameAs(metaclass1));
+ }
+
+ [Test]
+ public void VerifyComputeEvaluateFeatureOperation()
+ {
+ // Null guard on subject.
+ Assert.That(() => ((IMetadataFeature)null).ComputeEvaluateFeatureOperation(new Feature()), Throws.TypeOf());
+
+ var metadataFeature = new MetadataFeature();
+ var baseFeature = new Feature();
+
+ // Null baseFeature: throws ArgumentNullException (matching subject null-guard convention).
+ Assert.That(() => metadataFeature.ComputeEvaluateFeatureOperation(null), Throws.TypeOf());
+
+ // Empty feature list: MetadataFeature with no feature members → [].
+ Assert.That(metadataFeature.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
+
+ // Negative discrimination: feature present whose redefinition closure does NOT include baseFeature → [].
+ var unrelatedFeature = new Feature();
+ var unrelatedMembership = new FeatureMembership();
+ metadataFeature.AssignOwnership(unrelatedMembership, unrelatedFeature);
+
+ Assert.That(metadataFeature.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
+
+ // Direct match: candidate IS baseFeature (closure includes start node).
+ // No FeatureValue owned by the closure → returns [].
+ var metadataFeature2 = new MetadataFeature();
+ var directMembership = new FeatureMembership();
+ metadataFeature2.AssignOwnership(directMembership, baseFeature);
+
+ Assert.That(metadataFeature2.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
+
+ // Transitive match: candidate redefines baseFeature via a one-step chain.
+ // No FeatureValue → still returns [].
+ var metadataFeature3 = new MetadataFeature();
+ var transitiveCandidate = new Feature();
+ var transitiveRedefinition = new Redefinition { RedefinedFeature = baseFeature };
+ transitiveCandidate.AssignOwnership(transitiveRedefinition);
+
+ var transitiveMembership = new FeatureMembership();
+ metadataFeature3.AssignOwnership(transitiveMembership, transitiveCandidate);
+
+ Assert.That(metadataFeature3.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
+
+ // Cycle test: featureA.ownedRedefinition → featureB, featureB.ownedRedefinition → featureA.
+ // Closure helper must terminate; if the cycle does not include baseFeature → [].
+ var metadataFeature4 = new MetadataFeature();
+ var cycleFeatureA = new Feature();
+ var cycleFeatureB = new Feature();
+
+ var redefinitionAtoB = new Redefinition { RedefinedFeature = cycleFeatureB };
+ cycleFeatureA.AssignOwnership(redefinitionAtoB);
+
+ var redefinitionBtoA = new Redefinition { RedefinedFeature = cycleFeatureA };
+ cycleFeatureB.AssignOwnership(redefinitionBtoA);
+
+ var cycleMembership = new FeatureMembership();
+ metadataFeature4.AssignOwnership(cycleMembership, cycleFeatureA);
+
+ // Must not throw or infinite-loop; cycle does not include baseFeature → terminates with [].
+ Assert.That(() => metadataFeature4.ComputeEvaluateFeatureOperation(baseFeature), Throws.Nothing);
+ Assert.That(metadataFeature4.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
+
+ // Matched candidate with a FeatureValue whose value is a non-null Expression that has
+ // NO ResultExpressionMembership wired. Expression.Evaluate returns an empty list, so
+ // the operation returns the same empty list. (The stub-blocker case where Evaluate
+ // throws NotSupportedException via ResultExpressionMembershipExtensions.ComputeOwnedResultExpression
+ // cannot be wired here without modifying the sibling stub — scope discipline.)
+ var metadataFeature5 = new MetadataFeature();
+ var matchingFeature = new Feature();
+ var redefinitionToBase = new Redefinition { RedefinedFeature = baseFeature };
+ matchingFeature.AssignOwnership(redefinitionToBase);
+
+ var featureValue = new FeatureValue();
+ var valueExpression = new Expression();
+ matchingFeature.AssignOwnership(featureValue, valueExpression);
+
+ var matchingMembership = new FeatureMembership();
+ metadataFeature5.AssignOwnership(matchingMembership, matchingFeature);
+
+ Assert.That(metadataFeature5.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
+ }
+
+ [Test]
+ public void VerifyComputeIsSemanticOperation()
+ {
+ // Null guard.
+ Assert.That(() => ((IMetadataFeature)null).ComputeIsSemanticOperation(), Throws.TypeOf());
+
+ var metadataFeature = new MetadataFeature();
+
+ // Empty: no library specialization context → SpecializesFromLibrary("Metaobjects::SemanticMetadata")
+ // resolves to null → returns false.
+ Assert.That(metadataFeature.ComputeIsSemanticOperation(), Is.False);
+
+ // Discrimination: having a supertype that is a different library element still returns false.
+ // Wire a non-SemanticMetadata OwningMembership-contained type so the specialization chain is
+ // present but does not match the "Metaobjects::SemanticMetadata" qualified name.
+ var metadataFeature2 = new MetadataFeature();
+ var unrelatedSupertype = new Feature();
+ var subsetting = new Subsetting { SubsettedFeature = unrelatedSupertype };
+ metadataFeature2.AssignOwnership(subsetting);
+
+ Assert.That(metadataFeature2.ComputeIsSemanticOperation(), Is.False);
+
+ // NOTE: Wiring a true positive (returns true) requires constructing a "Metaobjects::SemanticMetadata"
+ // library namespace reachable via ResolveGlobal, which is infrastructure not available in unit tests.
+ // Integration-level coverage is required for the true-positive path.
+ }
+
+ [Test]
+ public void VerifyComputeIsSyntacticOperation()
+ {
+ // Null guard.
+ Assert.That(() => ((IMetadataFeature)null).ComputeIsSyntacticOperation(), Throws.TypeOf());
+
+ var metadataFeature = new MetadataFeature();
+
+ // Empty: no library specialization context → SpecializesFromLibrary("KerML::Element")
+ // resolves to null → returns false.
+ Assert.That(metadataFeature.ComputeIsSyntacticOperation(), Is.False);
+
+ // Discrimination: having a supertype that is a different library element still returns false.
+ var metadataFeature2 = new MetadataFeature();
+ var unrelatedSupertype = new Feature();
+ var subsetting = new Subsetting { SubsettedFeature = unrelatedSupertype };
+ metadataFeature2.AssignOwnership(subsetting);
+
+ Assert.That(metadataFeature2.ComputeIsSyntacticOperation(), Is.False);
+
+ // NOTE: Wiring a true positive (returns true) requires constructing a "KerML::Element" library
+ // namespace reachable via ResolveGlobal, which is infrastructure not available in unit tests.
+ // Integration-level coverage is required for the true-positive path.
+ }
+
+ [Test]
+ public void VerifyComputeSyntaxElementOperation_ThrowsNotSupportedException()
{
- Assert.That(() => ((IMetadataFeature)null).ComputeMetaclass(), Throws.TypeOf());
+ // Pending MOF reflective metaclass registry. Follow-up issue required.
+ // The KerML spec defines this operation with body "No OCL"; computing syntaxElement requires
+ // an inverse map from a runtime MetadataFeature back to the Element it reflects — infrastructure
+ // that does not yet exist in this SDK.
+ Assert.That(() => new MetadataFeature().ComputeSyntaxElementOperation(), Throws.TypeOf());
}
}
}
diff --git a/SysML2.NET/Extend/MetadataFeatureExtensions.cs b/SysML2.NET/Extend/MetadataFeatureExtensions.cs
index cc84d248..87cc35d6 100644
--- a/SysML2.NET/Extend/MetadataFeatureExtensions.cs
+++ b/SysML2.NET/Extend/MetadataFeatureExtensions.cs
@@ -22,14 +22,11 @@ namespace SysML2.NET.Core.POCO.Kernel.Metadata
{
using System;
using System.Collections.Generic;
+ using System.Linq;
- using SysML2.NET.Core.Core.Types;
- using SysML2.NET.Core.Root.Namespaces;
using SysML2.NET.Core.POCO.Core.Features;
- using SysML2.NET.Core.POCO.Core.Types;
- using SysML2.NET.Core.POCO.Root.Annotations;
+ using SysML2.NET.Core.POCO.Kernel.FeatureValues;
using SysML2.NET.Core.POCO.Root.Elements;
- using SysML2.NET.Core.POCO.Root.Namespaces;
///
/// The class provides extensions methods for
@@ -56,10 +53,16 @@ internal static class MetadataFeatureExtensions
///
/// the computed result
///
- [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static IMetaclass ComputeMetaclass(this IMetadataFeature metadataFeatureSubject)
{
- throw new NotSupportedException("Create a GitHub issue when this method is required");
+ if (metadataFeatureSubject == null)
+ {
+ throw new ArgumentNullException(nameof(metadataFeatureSubject));
+ }
+
+ var metaclassTypes = metadataFeatureSubject.type.OfType().ToList();
+
+ return metaclassTypes.Count == 0 ? null : metaclassTypes[0];
}
///
@@ -88,15 +91,48 @@ internal static IMetaclass ComputeMetaclass(this IMetadataFeature metadataFeatur
/// The subject
///
///
- /// No documentation provided
+ /// The base to look up in the redefinition closure of each feature
+ /// owned by the subject.
///
///
/// The expected collection of
///
- [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static List ComputeEvaluateFeatureOperation(this IMetadataFeature metadataFeatureSubject, IFeature baseFeature)
{
- throw new NotSupportedException("Create a GitHub issue when this method is required");
+ if (metadataFeatureSubject == null)
+ {
+ throw new ArgumentNullException(nameof(metadataFeatureSubject));
+ }
+
+ if (baseFeature == null)
+ {
+ throw new ArgumentNullException(nameof(baseFeature));
+ }
+
+ var selectedFeatures = metadataFeatureSubject.feature
+ .Where(feature => ComputeRedefinitionClosure(feature).Contains(baseFeature))
+ .ToList();
+
+ if (selectedFeatures.Count == 0)
+ {
+ return [];
+ }
+
+ var selectedFeature = selectedFeatures[0];
+
+ var featureValues = ComputeRedefinitionClosure(selectedFeature)
+ .SelectMany(feature => feature.ownedMember)
+ .OfType()
+ .ToList();
+
+ if (featureValues.Count == 0)
+ {
+ return [];
+ }
+
+ var valueExpression = featureValues[0].value;
+
+ return valueExpression == null ? [] : valueExpression.Evaluate(metadataFeatureSubject);
}
///
@@ -114,10 +150,11 @@ internal static List ComputeEvaluateFeatureOperation(this IMetadataFea
///
/// The expected
///
- [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static bool ComputeIsSemanticOperation(this IMetadataFeature metadataFeatureSubject)
{
- throw new NotSupportedException("Create a GitHub issue when this method is required");
+ return metadataFeatureSubject == null
+ ? throw new ArgumentNullException(nameof(metadataFeatureSubject))
+ : metadataFeatureSubject.SpecializesFromLibrary("Metaobjects::SemanticMetadata");
}
///
@@ -136,10 +173,11 @@ internal static bool ComputeIsSemanticOperation(this IMetadataFeature metadataFe
///
/// The expected
///
- [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static bool ComputeIsSyntacticOperation(this IMetadataFeature metadataFeatureSubject)
{
- throw new NotSupportedException("Create a GitHub issue when this method is required");
+ return metadataFeatureSubject == null
+ ? throw new ArgumentNullException(nameof(metadataFeatureSubject))
+ : metadataFeatureSubject.SpecializesFromLibrary("KerML::Element");
}
///
@@ -165,7 +203,47 @@ internal static bool ComputeIsSyntacticOperation(this IMetadataFeature metadataF
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static IElement ComputeSyntaxElementOperation(this IMetadataFeature metadataFeatureSubject)
{
+ // Implementation deferred: requires a MOF reflective metaclass registry
+ // (runtime MetadataFeature -> reflected IElement) that is not present in this SDK.
throw new NotSupportedException("Create a GitHub issue when this method is required");
}
+
+ ///
+ /// Computes the reflexive-transitive closure of over
+ /// ownedRedefinition.RedefinedFeature, using a HashSet visited-set for cycle protection.
+ ///
+ ///
+ /// The seed to start the closure from. The seed itself is included in
+ /// the result when non-null.
+ ///
+ ///
+ /// A fresh containing the seed and all transitively redefined features,
+ /// in BFS order. Returns an empty list when is null.
+ ///
+ private static List ComputeRedefinitionClosure(IFeature start)
+ {
+ var visited = new HashSet();
+ var result = new List();
+ var queue = new Queue();
+
+ if (start != null && visited.Add(start))
+ {
+ queue.Enqueue(start);
+ result.Add(start);
+ }
+
+ while (queue.Count > 0)
+ {
+ var current = queue.Dequeue();
+
+ foreach (var redefinedFeature in current.ownedRedefinition.Select(x => x.RedefinedFeature).Where(x => x != null && visited.Add(x)))
+ {
+ queue.Enqueue(redefinedFeature);
+ result.Add(redefinedFeature);
+ }
+ }
+
+ return result;
+ }
}
}