From c2254e733794e7dfd641066ca281dd7f6833900c Mon Sep 17 00:00:00 2001 From: atheate Date: Fri, 22 May 2026 14:38:30 +0200 Subject: [PATCH] Fix #154 --- .../MetadataFeatureExtensionsTestFixture.cs | 193 +++++++++++++++++- .../Extend/MetadataFeatureExtensions.cs | 106 ++++++++-- 2 files changed, 275 insertions(+), 24 deletions(-) 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; + } } }