Skip to content

Commit c2254e7

Browse files
Fix #154
1 parent df5f3a1 commit c2254e7

2 files changed

Lines changed: 275 additions & 24 deletions

File tree

Lines changed: 183 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,211 @@
1-
// -------------------------------------------------------------------------------------------------
1+
// -------------------------------------------------------------------------------------------------
22
// <copyright file="MetadataFeatureExtensionsTestFixture.cs" company="Starion Group S.A.">
3-
//
3+
//
44
// Copyright 2022-2026 Starion Group S.A.
5-
//
5+
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
88
// You may obtain a copy of the License at
9-
//
9+
//
1010
// http://www.apache.org/licenses/LICENSE-2.0
11-
//
11+
//
1212
// Unless required by applicable law or agreed to in writing, software
1313
// distributed under the License is distributed on an "AS IS" BASIS,
1414
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1515
// See the License for the specific language governing permissions and
1616
// limitations under the License.
17-
//
17+
//
1818
// </copyright>
1919
// ------------------------------------------------------------------------------------------------
2020

2121
namespace SysML2.NET.Tests.Extend
2222
{
2323
using System;
24-
24+
2525
using NUnit.Framework;
26-
26+
27+
using SysML2.NET.Core.POCO.Core.Features;
28+
using SysML2.NET.Core.POCO.Core.Types;
29+
using SysML2.NET.Core.POCO.Kernel.FeatureValues;
30+
using SysML2.NET.Core.POCO.Kernel.Functions;
2731
using SysML2.NET.Core.POCO.Kernel.Metadata;
32+
using SysML2.NET.Extensions;
2833

2934
[TestFixture]
3035
public class MetadataFeatureExtensionsTestFixture
3136
{
3237
[Test]
33-
public void ComputeMetaclass_ThrowsNotSupportedException()
38+
public void VerifyComputeMetaclass()
39+
{
40+
Assert.That(() => ((IMetadataFeature)null).ComputeMetaclass(), Throws.TypeOf<ArgumentNullException>());
41+
42+
var metadataFeature = new MetadataFeature();
43+
44+
// Empty: no FeatureTyping → null.
45+
Assert.That(metadataFeature.ComputeMetaclass(), Is.Null);
46+
47+
// Negative: FeatureTyping pointing at a non-Metaclass Type → null.
48+
var nonMetaclassType = new FeatureTyping { Type = new Feature() };
49+
metadataFeature.AssignOwnership(nonMetaclassType);
50+
51+
Assert.That(metadataFeature.ComputeMetaclass(), Is.Null);
52+
53+
// Positive: FeatureTyping pointing at a Metaclass → returned.
54+
var metaclass1 = new Metaclass();
55+
var typingToMetaclass1 = new FeatureTyping { Type = metaclass1 };
56+
metadataFeature.AssignOwnership(typingToMetaclass1);
57+
58+
Assert.That(metadataFeature.ComputeMetaclass(), Is.SameAs(metaclass1));
59+
60+
// Multiple Metaclass typings → first returned (insertion order).
61+
var metaclass2 = new Metaclass();
62+
var typingToMetaclass2 = new FeatureTyping { Type = metaclass2 };
63+
metadataFeature.AssignOwnership(typingToMetaclass2);
64+
65+
Assert.That(metadataFeature.ComputeMetaclass(), Is.SameAs(metaclass1));
66+
}
67+
68+
[Test]
69+
public void VerifyComputeEvaluateFeatureOperation()
70+
{
71+
// Null guard on subject.
72+
Assert.That(() => ((IMetadataFeature)null).ComputeEvaluateFeatureOperation(new Feature()), Throws.TypeOf<ArgumentNullException>());
73+
74+
var metadataFeature = new MetadataFeature();
75+
var baseFeature = new Feature();
76+
77+
// Null baseFeature: throws ArgumentNullException (matching subject null-guard convention).
78+
Assert.That(() => metadataFeature.ComputeEvaluateFeatureOperation(null), Throws.TypeOf<ArgumentNullException>());
79+
80+
// Empty feature list: MetadataFeature with no feature members → [].
81+
Assert.That(metadataFeature.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
82+
83+
// Negative discrimination: feature present whose redefinition closure does NOT include baseFeature → [].
84+
var unrelatedFeature = new Feature();
85+
var unrelatedMembership = new FeatureMembership();
86+
metadataFeature.AssignOwnership(unrelatedMembership, unrelatedFeature);
87+
88+
Assert.That(metadataFeature.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
89+
90+
// Direct match: candidate IS baseFeature (closure includes start node).
91+
// No FeatureValue owned by the closure → returns [].
92+
var metadataFeature2 = new MetadataFeature();
93+
var directMembership = new FeatureMembership();
94+
metadataFeature2.AssignOwnership(directMembership, baseFeature);
95+
96+
Assert.That(metadataFeature2.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
97+
98+
// Transitive match: candidate redefines baseFeature via a one-step chain.
99+
// No FeatureValue → still returns [].
100+
var metadataFeature3 = new MetadataFeature();
101+
var transitiveCandidate = new Feature();
102+
var transitiveRedefinition = new Redefinition { RedefinedFeature = baseFeature };
103+
transitiveCandidate.AssignOwnership(transitiveRedefinition);
104+
105+
var transitiveMembership = new FeatureMembership();
106+
metadataFeature3.AssignOwnership(transitiveMembership, transitiveCandidate);
107+
108+
Assert.That(metadataFeature3.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
109+
110+
// Cycle test: featureA.ownedRedefinition → featureB, featureB.ownedRedefinition → featureA.
111+
// Closure helper must terminate; if the cycle does not include baseFeature → [].
112+
var metadataFeature4 = new MetadataFeature();
113+
var cycleFeatureA = new Feature();
114+
var cycleFeatureB = new Feature();
115+
116+
var redefinitionAtoB = new Redefinition { RedefinedFeature = cycleFeatureB };
117+
cycleFeatureA.AssignOwnership(redefinitionAtoB);
118+
119+
var redefinitionBtoA = new Redefinition { RedefinedFeature = cycleFeatureA };
120+
cycleFeatureB.AssignOwnership(redefinitionBtoA);
121+
122+
var cycleMembership = new FeatureMembership();
123+
metadataFeature4.AssignOwnership(cycleMembership, cycleFeatureA);
124+
125+
// Must not throw or infinite-loop; cycle does not include baseFeature → terminates with [].
126+
Assert.That(() => metadataFeature4.ComputeEvaluateFeatureOperation(baseFeature), Throws.Nothing);
127+
Assert.That(metadataFeature4.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
128+
129+
// Matched candidate with a FeatureValue whose value is a non-null Expression that has
130+
// NO ResultExpressionMembership wired. Expression.Evaluate returns an empty list, so
131+
// the operation returns the same empty list. (The stub-blocker case where Evaluate
132+
// throws NotSupportedException via ResultExpressionMembershipExtensions.ComputeOwnedResultExpression
133+
// cannot be wired here without modifying the sibling stub — scope discipline.)
134+
var metadataFeature5 = new MetadataFeature();
135+
var matchingFeature = new Feature();
136+
var redefinitionToBase = new Redefinition { RedefinedFeature = baseFeature };
137+
matchingFeature.AssignOwnership(redefinitionToBase);
138+
139+
var featureValue = new FeatureValue();
140+
var valueExpression = new Expression();
141+
matchingFeature.AssignOwnership(featureValue, valueExpression);
142+
143+
var matchingMembership = new FeatureMembership();
144+
metadataFeature5.AssignOwnership(matchingMembership, matchingFeature);
145+
146+
Assert.That(metadataFeature5.ComputeEvaluateFeatureOperation(baseFeature), Is.Empty);
147+
}
148+
149+
[Test]
150+
public void VerifyComputeIsSemanticOperation()
151+
{
152+
// Null guard.
153+
Assert.That(() => ((IMetadataFeature)null).ComputeIsSemanticOperation(), Throws.TypeOf<ArgumentNullException>());
154+
155+
var metadataFeature = new MetadataFeature();
156+
157+
// Empty: no library specialization context → SpecializesFromLibrary("Metaobjects::SemanticMetadata")
158+
// resolves to null → returns false.
159+
Assert.That(metadataFeature.ComputeIsSemanticOperation(), Is.False);
160+
161+
// Discrimination: having a supertype that is a different library element still returns false.
162+
// Wire a non-SemanticMetadata OwningMembership-contained type so the specialization chain is
163+
// present but does not match the "Metaobjects::SemanticMetadata" qualified name.
164+
var metadataFeature2 = new MetadataFeature();
165+
var unrelatedSupertype = new Feature();
166+
var subsetting = new Subsetting { SubsettedFeature = unrelatedSupertype };
167+
metadataFeature2.AssignOwnership(subsetting);
168+
169+
Assert.That(metadataFeature2.ComputeIsSemanticOperation(), Is.False);
170+
171+
// NOTE: Wiring a true positive (returns true) requires constructing a "Metaobjects::SemanticMetadata"
172+
// library namespace reachable via ResolveGlobal, which is infrastructure not available in unit tests.
173+
// Integration-level coverage is required for the true-positive path.
174+
}
175+
176+
[Test]
177+
public void VerifyComputeIsSyntacticOperation()
178+
{
179+
// Null guard.
180+
Assert.That(() => ((IMetadataFeature)null).ComputeIsSyntacticOperation(), Throws.TypeOf<ArgumentNullException>());
181+
182+
var metadataFeature = new MetadataFeature();
183+
184+
// Empty: no library specialization context → SpecializesFromLibrary("KerML::Element")
185+
// resolves to null → returns false.
186+
Assert.That(metadataFeature.ComputeIsSyntacticOperation(), Is.False);
187+
188+
// Discrimination: having a supertype that is a different library element still returns false.
189+
var metadataFeature2 = new MetadataFeature();
190+
var unrelatedSupertype = new Feature();
191+
var subsetting = new Subsetting { SubsettedFeature = unrelatedSupertype };
192+
metadataFeature2.AssignOwnership(subsetting);
193+
194+
Assert.That(metadataFeature2.ComputeIsSyntacticOperation(), Is.False);
195+
196+
// NOTE: Wiring a true positive (returns true) requires constructing a "KerML::Element" library
197+
// namespace reachable via ResolveGlobal, which is infrastructure not available in unit tests.
198+
// Integration-level coverage is required for the true-positive path.
199+
}
200+
201+
[Test]
202+
public void VerifyComputeSyntaxElementOperation_ThrowsNotSupportedException()
34203
{
35-
Assert.That(() => ((IMetadataFeature)null).ComputeMetaclass(), Throws.TypeOf<NotSupportedException>());
204+
// Pending MOF reflective metaclass registry. Follow-up issue required.
205+
// The KerML spec defines this operation with body "No OCL"; computing syntaxElement requires
206+
// an inverse map from a runtime MetadataFeature back to the Element it reflects — infrastructure
207+
// that does not yet exist in this SDK.
208+
Assert.That(() => new MetadataFeature().ComputeSyntaxElementOperation(), Throws.TypeOf<NotSupportedException>());
36209
}
37210
}
38211
}

SysML2.NET/Extend/MetadataFeatureExtensions.cs

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,11 @@ namespace SysML2.NET.Core.POCO.Kernel.Metadata
2222
{
2323
using System;
2424
using System.Collections.Generic;
25+
using System.Linq;
2526

26-
using SysML2.NET.Core.Core.Types;
27-
using SysML2.NET.Core.Root.Namespaces;
2827
using SysML2.NET.Core.POCO.Core.Features;
29-
using SysML2.NET.Core.POCO.Core.Types;
30-
using SysML2.NET.Core.POCO.Root.Annotations;
28+
using SysML2.NET.Core.POCO.Kernel.FeatureValues;
3129
using SysML2.NET.Core.POCO.Root.Elements;
32-
using SysML2.NET.Core.POCO.Root.Namespaces;
3330

3431
/// <summary>
3532
/// The <see cref="MetadataFeatureExtensions"/> class provides extensions methods for
@@ -56,10 +53,16 @@ internal static class MetadataFeatureExtensions
5653
/// <returns>
5754
/// the computed result
5855
/// </returns>
59-
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
6056
internal static IMetaclass ComputeMetaclass(this IMetadataFeature metadataFeatureSubject)
6157
{
62-
throw new NotSupportedException("Create a GitHub issue when this method is required");
58+
if (metadataFeatureSubject == null)
59+
{
60+
throw new ArgumentNullException(nameof(metadataFeatureSubject));
61+
}
62+
63+
var metaclassTypes = metadataFeatureSubject.type.OfType<IMetaclass>().ToList();
64+
65+
return metaclassTypes.Count == 0 ? null : metaclassTypes[0];
6366
}
6467

6568
/// <summary>
@@ -88,15 +91,48 @@ internal static IMetaclass ComputeMetaclass(this IMetadataFeature metadataFeatur
8891
/// The subject <see cref="IMetadataFeature"/>
8992
/// </param>
9093
/// <param name="baseFeature">
91-
/// No documentation provided
94+
/// The base <see cref="IFeature"/> to look up in the redefinition closure of each feature
95+
/// owned by the subject.
9296
/// </param>
9397
/// <returns>
9498
/// The expected collection of <see cref="IElement" />
9599
/// </returns>
96-
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
97100
internal static List<IElement> ComputeEvaluateFeatureOperation(this IMetadataFeature metadataFeatureSubject, IFeature baseFeature)
98101
{
99-
throw new NotSupportedException("Create a GitHub issue when this method is required");
102+
if (metadataFeatureSubject == null)
103+
{
104+
throw new ArgumentNullException(nameof(metadataFeatureSubject));
105+
}
106+
107+
if (baseFeature == null)
108+
{
109+
throw new ArgumentNullException(nameof(baseFeature));
110+
}
111+
112+
var selectedFeatures = metadataFeatureSubject.feature
113+
.Where(feature => ComputeRedefinitionClosure(feature).Contains(baseFeature))
114+
.ToList();
115+
116+
if (selectedFeatures.Count == 0)
117+
{
118+
return [];
119+
}
120+
121+
var selectedFeature = selectedFeatures[0];
122+
123+
var featureValues = ComputeRedefinitionClosure(selectedFeature)
124+
.SelectMany(feature => feature.ownedMember)
125+
.OfType<IFeatureValue>()
126+
.ToList();
127+
128+
if (featureValues.Count == 0)
129+
{
130+
return [];
131+
}
132+
133+
var valueExpression = featureValues[0].value;
134+
135+
return valueExpression == null ? [] : valueExpression.Evaluate(metadataFeatureSubject);
100136
}
101137

102138
/// <summary>
@@ -114,10 +150,11 @@ internal static List<IElement> ComputeEvaluateFeatureOperation(this IMetadataFea
114150
/// <returns>
115151
/// The expected <see cref="bool" />
116152
/// </returns>
117-
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
118153
internal static bool ComputeIsSemanticOperation(this IMetadataFeature metadataFeatureSubject)
119154
{
120-
throw new NotSupportedException("Create a GitHub issue when this method is required");
155+
return metadataFeatureSubject == null
156+
? throw new ArgumentNullException(nameof(metadataFeatureSubject))
157+
: metadataFeatureSubject.SpecializesFromLibrary("Metaobjects::SemanticMetadata");
121158
}
122159

123160
/// <summary>
@@ -136,10 +173,11 @@ internal static bool ComputeIsSemanticOperation(this IMetadataFeature metadataFe
136173
/// <returns>
137174
/// The expected <see cref="bool" />
138175
/// </returns>
139-
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
140176
internal static bool ComputeIsSyntacticOperation(this IMetadataFeature metadataFeatureSubject)
141177
{
142-
throw new NotSupportedException("Create a GitHub issue when this method is required");
178+
return metadataFeatureSubject == null
179+
? throw new ArgumentNullException(nameof(metadataFeatureSubject))
180+
: metadataFeatureSubject.SpecializesFromLibrary("KerML::Element");
143181
}
144182

145183
/// <summary>
@@ -165,7 +203,47 @@ internal static bool ComputeIsSyntacticOperation(this IMetadataFeature metadataF
165203
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
166204
internal static IElement ComputeSyntaxElementOperation(this IMetadataFeature metadataFeatureSubject)
167205
{
206+
// Implementation deferred: requires a MOF reflective metaclass registry
207+
// (runtime MetadataFeature -> reflected IElement) that is not present in this SDK.
168208
throw new NotSupportedException("Create a GitHub issue when this method is required");
169209
}
210+
211+
/// <summary>
212+
/// Computes the reflexive-transitive closure of <paramref name="start"/> over
213+
/// <c>ownedRedefinition.RedefinedFeature</c>, using a HashSet visited-set for cycle protection.
214+
/// </summary>
215+
/// <param name="start">
216+
/// The seed <see cref="IFeature"/> to start the closure from. The seed itself is included in
217+
/// the result when non-null.
218+
/// </param>
219+
/// <returns>
220+
/// A fresh <see cref="List{T}"/> containing the seed and all transitively redefined features,
221+
/// in BFS order. Returns an empty list when <paramref name="start"/> is null.
222+
/// </returns>
223+
private static List<IFeature> ComputeRedefinitionClosure(IFeature start)
224+
{
225+
var visited = new HashSet<IFeature>();
226+
var result = new List<IFeature>();
227+
var queue = new Queue<IFeature>();
228+
229+
if (start != null && visited.Add(start))
230+
{
231+
queue.Enqueue(start);
232+
result.Add(start);
233+
}
234+
235+
while (queue.Count > 0)
236+
{
237+
var current = queue.Dequeue();
238+
239+
foreach (var redefinedFeature in current.ownedRedefinition.Select(x => x.RedefinedFeature).Where(x => x != null && visited.Add(x)))
240+
{
241+
queue.Enqueue(redefinedFeature);
242+
result.Add(redefinedFeature);
243+
}
244+
}
245+
246+
return result;
247+
}
170248
}
171249
}

0 commit comments

Comments
 (0)