From a3fae603d945a7f277ac86d6b37c88db1910a787 Mon Sep 17 00:00:00 2001 From: atheate Date: Tue, 26 May 2026 10:51:20 +0200 Subject: [PATCH 1/2] Fix #186 --- ...uirementDefinitionExtensionsTestFixture.cs | 8 ++- .../RequirementUsageExtensionsTestFixture.cs | 12 ++-- .../SubjectMembershipExtensionsTestFixture.cs | 59 +++++++++++++++---- .../Extend/SubjectMembershipExtensions.cs | 20 +++---- 4 files changed, 70 insertions(+), 29 deletions(-) diff --git a/SysML2.NET.Tests/Extend/RequirementDefinitionExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/RequirementDefinitionExtensionsTestFixture.cs index 2af5cd12..6389e0f6 100644 --- a/SysML2.NET.Tests/Extend/RequirementDefinitionExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/RequirementDefinitionExtensionsTestFixture.cs @@ -196,13 +196,15 @@ public void VerifyComputeSubjectParameter() Assert.That(requirementDefinition.ComputeSubjectParameter(), Is.Null); - // Populated case: SubjectMembership is present; selecting ownedSubjectParameter triggers an - // upstream stub (SubjectMembershipExtensions.ComputeOwnedSubjectParameter is not yet implemented). + // Populated case: SubjectMembership is present alongside the earlier ParameterMembership. + // OfType must discriminate — only the subject's ownedSubjectParameter surfaces. + // This also covers the mixed-state discrimination: both a ParameterMembership and a SubjectMembership + // are wired; the result must be the subject usage, not the parameter usage. var subjectMembership = new SubjectMembership(); var subjectUsage = new Usage(); requirementDefinition.AssignOwnership(subjectMembership, subjectUsage); - Assert.That(() => requirementDefinition.ComputeSubjectParameter(), Throws.TypeOf()); + Assert.That(requirementDefinition.ComputeSubjectParameter(), Is.SameAs(subjectUsage)); } private static readonly string[] ExpectedSingleComputedText = ["The requirement text."]; diff --git a/SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs index 1c7906f7..24023e15 100644 --- a/SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/RequirementUsageExtensionsTestFixture.cs @@ -229,17 +229,19 @@ public void VerifyComputeSubjectParameter() Assert.That(requirementUsage.ComputeSubjectParameter(), Is.Null); - // Populated case: SubjectMembership is present; selecting ownedSubjectParameter triggers an - // upstream stub (SubjectMembershipExtensions.ComputeOwnedSubjectParameter is not yet implemented). + // Populated case: SubjectMembership is present alongside the earlier ParameterMembership. + // OfType must discriminate — only the subject's ownedSubjectParameter surfaces. + // This also covers the mixed-state discrimination: both a ParameterMembership and a SubjectMembership + // are wired; the result must be the subject usage, not the parameter usage. var subjectMembership = new SubjectMembership(); var subjectUsage = new Usage(); requirementUsage.AssignOwnership(subjectMembership, subjectUsage); - Assert.That(() => requirementUsage.ComputeSubjectParameter(), Throws.TypeOf()); + Assert.That(requirementUsage.ComputeSubjectParameter(), Is.SameAs(subjectUsage)); } - private static readonly string[] ExpectedSingleComputedText = new[] { "The requirement text." }; - private static readonly string[] ExpectedMultipleComputedText = new[] { "The requirement text.", "Additional context." }; + private static readonly string[] ExpectedSingleComputedText = ["The requirement text."]; + private static readonly string[] ExpectedMultipleComputedText = ["The requirement text.", "Additional context."]; [Test] public void VerifyComputeText() diff --git a/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs index 8400a27a..e009d9f2 100644 --- a/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs @@ -1,38 +1,77 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // -// +// // 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.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; using SysML2.NET.Core.POCO.Systems.Requirements; + using SysML2.NET.Exceptions; + using SysML2.NET.Extensions; + + using Type = SysML2.NET.Core.POCO.Core.Types.Type; [TestFixture] public class SubjectMembershipExtensionsTestFixture { [Test] - public void ComputeOwnedSubjectParameter_ThrowsNotSupportedException() + public void VerifyComputeOwnedSubjectParameter() { - Assert.That(() => ((ISubjectMembership)null).ComputeOwnedSubjectParameter(), Throws.TypeOf()); + Assert.That(() => ((ISubjectMembership)null).ComputeOwnedSubjectParameter(), Throws.TypeOf()); + + var subjectMembership = new SubjectMembership(); + + Assert.That(() => subjectMembership.ComputeOwnedSubjectParameter(), Throws.TypeOf()); + + var owningType = new Type(); + var subjectUsage = new Usage(); + + owningType.AssignOwnership(subjectMembership, subjectUsage); + + Assert.That(subjectMembership.ComputeOwnedSubjectParameter(), Is.SameAs(subjectUsage)); + + // Wiring two usages to verify the multiple-element guard: + // First create a fresh membership with two elements via the backdoor. + var twoElementMembership = new SubjectMembership(); + var secondUsage = new Usage(); + + ((IContainedRelationship)twoElementMembership).OwnedRelatedElement.Add(subjectUsage); + ((IContainedRelationship)twoElementMembership).OwnedRelatedElement.Add(secondUsage); + + Assert.That(() => twoElementMembership.ComputeOwnedSubjectParameter(), Throws.TypeOf()); + + // NOTE: wiring a non-IUsage element as the sole OwnedRelatedElement is not possible via the + // public AssignOwnership API (ISubjectMembership requires an IUsage target). + // To cover the as-cast-returns-null path we directly populate OwnedRelatedElement with a + // plain Namespace (which is not an IUsage). + var nonUsageMembership = new SubjectMembership(); + var nonUsageElement = new Namespace(); + + ((IContainedRelationship)nonUsageMembership).OwnedRelatedElement.Add(nonUsageElement); + + Assert.That(nonUsageMembership.ComputeOwnedSubjectParameter(), Is.Null); } } } diff --git a/SysML2.NET/Extend/SubjectMembershipExtensions.cs b/SysML2.NET/Extend/SubjectMembershipExtensions.cs index 9cd8d5a0..d3841ee6 100644 --- a/SysML2.NET/Extend/SubjectMembershipExtensions.cs +++ b/SysML2.NET/Extend/SubjectMembershipExtensions.cs @@ -21,17 +21,9 @@ namespace SysML2.NET.Core.POCO.Systems.Requirements { using System; - using System.Collections.Generic; - 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.Kernel.Behaviors; - using SysML2.NET.Core.POCO.Root.Annotations; - using SysML2.NET.Core.POCO.Root.Elements; - using SysML2.NET.Core.POCO.Root.Namespaces; using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; + using SysML2.NET.Exceptions; /// /// The class provides extensions methods for @@ -48,10 +40,16 @@ internal static class SubjectMembershipExtensions /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IUsage ComputeOwnedSubjectParameter(this ISubjectMembership subjectMembershipSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (subjectMembershipSubject == null) + { + throw new ArgumentNullException(nameof(subjectMembershipSubject)); + } + + return subjectMembershipSubject.OwnedRelatedElement.Count != 1 + ? throw new IncompleteModelException($"{nameof(subjectMembershipSubject)} must have exactly one related element") + : subjectMembershipSubject.OwnedRelatedElement[0] as IUsage; } } From e5b5832e2c8d348c496c1b3c1f6226f0fb618228 Mon Sep 17 00:00:00 2001 From: atheate Date: Tue, 26 May 2026 11:46:06 +0200 Subject: [PATCH 2/2] Fix wrongly implemented [1] multiplicity --- CLAUDE.md | 25 ++++++++++++++ .../FeatureMembershipExtensionsTestFixture.cs | 32 ++++++++++++++--- .../FeatureValueExtensionsTestFixture.cs | 34 +++++++++++++++++-- ...arameterMembershipExtensionsTestFixture.cs | 33 ++++++++++++------ .../SubjectMembershipExtensionsTestFixture.cs | 34 +++++++++++++------ .../Extend/FeatureMembershipExtensions.cs | 9 +++-- SysML2.NET/Extend/FeatureValueExtensions.cs | 10 ++++-- .../Extend/ParameterMembershipExtensions.cs | 9 +++-- .../Extend/SubjectMembershipExtensions.cs | 10 ++++-- 9 files changed, 155 insertions(+), 41 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 36fb6f58..0023aa84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,3 +161,28 @@ Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package - Prefer C# property patterns ('x is IType { Prop: value }') over declared-variable-plus-predicate form ('x is IType name && name.Prop == value') when the narrowed variable is only consulted once; the property-pattern form is more concise and intent-revealing - Surround every braced block (`if`, `else if`, `while`, `for`, `foreach`, `switch`, `using`, `try`/`catch`/`finally`, `lock`, `do…while`, anonymous `{ }`) with a blank line on both sides — the rule does NOT apply at the very start/end of a method body, nor between a `}` and a continuation keyword (`else`, `catch`, `finally`, `while` of `do…while`) that belongs to the same control flow - When invoking an operation or derived property on a POCO from inside an extension method, call the POCO's instance member (e.g. `subject.IsDistinguishableFrom(other)`, `subject.qualifiedName`), NOT the static `ComputeXxxOperation` / `ComputeXxx` extension method. Virtual dispatch on the POCO honors operation/property REDEFINITION in subclass POCOs; calling the static extension directly bypasses dispatch and silently skips overrides. The static-extension form is reserved EXCLUSIVELY for the C# translation of OCL `self.oclAsType(SuperType).method()` — an explicit upcast that mandates targeting the SuperType's body (e.g. `Usage::namingFeature()` → `FeatureExtensions.ComputeNamingFeatureOperation(usage)`; `OwningMembership::path()` → `RelationshipExtensions.ComputeRedefinedPathOperation(owningMembership)`) +- **`IRelationship.OwnedRelatedElement` and `IElement.OwnedRelationship` storage collections are `[0..*]` — NEVER cardinality-limited.** The [1..1] / [0..1] multiplicities that appear in the metamodel apply to *derived* / *redefined* properties (e.g. `OwningMembership::ownedMemberElement`, `FeatureMembership::ownedMemberFeature`, `SubjectMembership::ownedSubjectParameter`), NOT to the underlying storage. When implementing such a derivation, **project from the collection — do not assume positional indexing**. The canonical "in-between" pattern is *filter-by-type-then-validate-count*: project with `OfType()`, then validate the projection count against the **derived property's declared multiplicity** (read it from the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface, or directly from the UML XMI). The failure mode depends on the multiplicity: + + | Multiplicity | Empty projection | Single-match projection | 2+ match projection | + |---|---|---|---| + | `[1..1]` (lowerValue=1, upperValue=1) | `throw IncompleteModelException` | return the match | `throw IncompleteModelException` | + | `[0..1]` (lowerValue=0, upperValue=1) | `return null` | return the match | `throw IncompleteModelException` | + | `[0..*]` / `[1..*]` | (use `List` projection; not this pattern) | n/a | n/a | + + `IncompleteModelException` is the loud signal to SDK users that the model is malformed — DO NOT swallow it as `null` when the multiplicity is `[1..1]`, and DO NOT raise it for the empty case when the multiplicity is `[0..1]` (a legitimately-optional property). + + ```csharp + // [1..1] type-narrowed redefinition (e.g. SubjectMembership::ownedSubjectParameter : IUsage) + var matches = subject.OwnedRelatedElement.OfType().ToList(); + + return matches.Count == 1 + ? matches[0] + : throw new IncompleteModelException($"{nameof(subject)} must have exactly one related element of type {nameof(ITargetType)}"); + + // [1..1] non-narrowing redefinition (e.g. OwningMembership::ownedMemberElement : IElement) + return subject.OwnedRelatedElement.Count == 1 + ? subject.OwnedRelatedElement[0] + : throw new IncompleteModelException($"{nameof(subject)} must have exactly one related element"); + ``` + + Do NOT use `.Count != 1 → throw` followed by `OwnedRelatedElement[0] as ITargetType` — that pattern silently drops the correctly-typed element when it does not sit at index 0 (e.g. when an `IAnnotation` target is also present, since `AssignOwnership` allows owned related elements for both `IOwningMembership` AND `IAnnotation`). Same rule applies to any other derived property that subsets one of these two `[0..*]` storage collections. diff --git a/SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs index e005e739..bb2258ed 100644 --- a/SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/FeatureMembershipExtensionsTestFixture.cs @@ -41,10 +41,12 @@ public void VerifyComputeOwnedMemberFeature() { Assert.That(() => ((IFeatureMembership)null).ComputeOwnedMemberFeature(), Throws.TypeOf()); + // Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. var featureMembership = new FeatureMembership(); Assert.That(() => featureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf()); + // Single IFeature wired via the public API → returned. var owningType = new Type(); var feature = new Feature(); @@ -52,16 +54,36 @@ public void VerifyComputeOwnedMemberFeature() Assert.That(featureMembership.ComputeOwnedMemberFeature(), Is.SameAs(feature)); - // NOTE: wiring a non-IFeature element as the sole OwnedRelatedElement is not possible via the - // public AssignOwnership API (it validates that FeatureMembership requires an IFeature target). - // To cover the as-cast-returns-null path we directly set OwningRelatedElement on a fresh - // membership so that OwnedRelatedElement[0] is a plain Namespace (which is not an IFeature). + // Two IFeatures in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + var twoFeatureMembership = new FeatureMembership(); + var firstFeature = new Feature(); + var secondFeature = new Feature(); + + ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(firstFeature); + ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(secondFeature); + + Assert.That(() => twoFeatureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf()); + + // Mixed-type owned related elements: exactly one IFeature alongside a non-IFeature (Namespace). + // The OfType() projection MUST pick out the IFeature regardless of its position + // (this is the core robustness guarantee — never positionally index the unfiltered collection). + var mixedMembership = new FeatureMembership(); + var siblingNonFeature = new Namespace(); + var mixedFeature = new Feature(); + + ((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(siblingNonFeature); + ((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(mixedFeature); + + Assert.That(mixedMembership.ComputeOwnedMemberFeature(), Is.SameAs(mixedFeature)); + + // OwnedRelatedElement populated with non-IFeature element(s) only → no IFeature match: + // [1..1] violation, throws IncompleteModelException. var nonFeatureMembership = new FeatureMembership(); var nonFeatureElement = new Namespace(); ((IContainedRelationship)nonFeatureMembership).OwnedRelatedElement.Add(nonFeatureElement); - Assert.That(nonFeatureMembership.ComputeOwnedMemberFeature(), Is.Null); + Assert.That(() => nonFeatureMembership.ComputeOwnedMemberFeature(), Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs index f3bdca61..f85a0986 100644 --- a/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs @@ -29,6 +29,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Kernel.FeatureValues; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Exceptions; using SysML2.NET.Extensions; [TestFixture] @@ -61,6 +62,12 @@ public void VerifyComputeValue() { Assert.That(() => ((IFeatureValue)null).ComputeValue(), Throws.TypeOf()); + // Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + var emptyFeatureValue = new FeatureValue(); + + Assert.That(() => emptyFeatureValue.ComputeValue(), Throws.TypeOf()); + + // Single IExpression wired via the public API → returned. var feature = new Feature(); var featureValue = new FeatureValue(); var literalBoolean = new LiteralBoolean(); @@ -69,13 +76,36 @@ public void VerifyComputeValue() Assert.That(featureValue.ComputeValue(), Is.SameAs(literalBoolean)); - // Non-Expression owned member: direct field bypass — the cast must return null. + // Two IExpressions in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + var twoExprFeatureValue = new FeatureValue(); + var firstExpression = new LiteralBoolean(); + var secondExpression = new LiteralBoolean(); + + ((IContainedRelationship)twoExprFeatureValue).OwnedRelatedElement.Add(firstExpression); + ((IContainedRelationship)twoExprFeatureValue).OwnedRelatedElement.Add(secondExpression); + + Assert.That(() => twoExprFeatureValue.ComputeValue(), Throws.TypeOf()); + + // Mixed-type owned related elements: exactly one IExpression alongside a non-IExpression (Namespace). + // The OfType() projection MUST pick out the IExpression regardless of its position + // (this is the core robustness guarantee — never positionally index the unfiltered collection). + var mixedFeatureValue = new FeatureValue(); + var siblingNonExpression = new Namespace(); + var mixedExpression = new LiteralBoolean(); + + ((IContainedRelationship)mixedFeatureValue).OwnedRelatedElement.Add(siblingNonExpression); + ((IContainedRelationship)mixedFeatureValue).OwnedRelatedElement.Add(mixedExpression); + + Assert.That(mixedFeatureValue.ComputeValue(), Is.SameAs(mixedExpression)); + + // OwnedRelatedElement populated with non-IExpression element(s) only → no IExpression match: + // [1..1] violation, throws IncompleteModelException. var nonExprFeatureValue = new FeatureValue(); var nonExprElement = new Namespace(); ((IContainedRelationship)nonExprFeatureValue).OwnedRelatedElement.Add(nonExprElement); - Assert.That(nonExprFeatureValue.ComputeValue(), Is.Null); + Assert.That(() => nonExprFeatureValue.ComputeValue(), Throws.TypeOf()); } } } diff --git a/SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs index 519f40bb..3006d92d 100644 --- a/SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ParameterMembershipExtensionsTestFixture.cs @@ -43,10 +43,12 @@ public void VerifyComputeOwnedMemberParameter() { Assert.That(() => ((IParameterMembership)null).ComputeOwnedMemberParameter(), Throws.TypeOf()); + // Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. var parameterMembership = new ParameterMembership(); Assert.That(() => parameterMembership.ComputeOwnedMemberParameter(), Throws.TypeOf()); + // Single IFeature wired via the public API → returned. var owningType = new Type(); var feature = new Feature(); @@ -54,26 +56,35 @@ public void VerifyComputeOwnedMemberParameter() Assert.That(parameterMembership.ComputeOwnedMemberParameter(), Is.SameAs(feature)); - // Wiring two features to verify the multiple-element guard: - // First remove the existing wiring so we can create a fresh membership with two elements. - var twoElementMembership = new ParameterMembership(); + // Two IFeatures in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + var twoFeatureMembership = new ParameterMembership(); var secondFeature = new Feature(); - ((IContainedRelationship)twoElementMembership).OwnedRelatedElement.Add(feature); - ((IContainedRelationship)twoElementMembership).OwnedRelatedElement.Add(secondFeature); + ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(feature); + ((IContainedRelationship)twoFeatureMembership).OwnedRelatedElement.Add(secondFeature); - Assert.That(() => twoElementMembership.ComputeOwnedMemberParameter(), Throws.TypeOf()); + Assert.That(() => twoFeatureMembership.ComputeOwnedMemberParameter(), Throws.TypeOf()); - // NOTE: wiring a non-IFeature element as the sole OwnedRelatedElement is not possible via the - // public AssignOwnership API (IParameterMembership requires an IFeature target). - // To cover the as-cast-returns-null path we directly populate OwnedRelatedElement with a - // plain Namespace (which is not an IFeature). + // Mixed-type owned related elements: exactly one IFeature alongside a non-IFeature (Namespace). + // The OfType() projection MUST pick out the IFeature regardless of its position + // (this is the core robustness guarantee — never positionally index the unfiltered collection). + var mixedMembership = new ParameterMembership(); + var siblingNonFeature = new Namespace(); + var mixedFeature = new Feature(); + + ((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(siblingNonFeature); + ((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(mixedFeature); + + Assert.That(mixedMembership.ComputeOwnedMemberParameter(), Is.SameAs(mixedFeature)); + + // OwnedRelatedElement populated with non-IFeature element(s) only → no IFeature match: + // [1..1] violation, throws IncompleteModelException. var nonFeatureMembership = new ParameterMembership(); var nonFeatureElement = new Namespace(); ((IContainedRelationship)nonFeatureMembership).OwnedRelatedElement.Add(nonFeatureElement); - Assert.That(nonFeatureMembership.ComputeOwnedMemberParameter(), Is.Null); + Assert.That(() => nonFeatureMembership.ComputeOwnedMemberParameter(), Throws.TypeOf()); } [Test] diff --git a/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs index e009d9f2..2a0e89af 100644 --- a/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/SubjectMembershipExtensionsTestFixture.cs @@ -41,10 +41,12 @@ public void VerifyComputeOwnedSubjectParameter() { Assert.That(() => ((ISubjectMembership)null).ComputeOwnedSubjectParameter(), Throws.TypeOf()); + // Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. var subjectMembership = new SubjectMembership(); Assert.That(() => subjectMembership.ComputeOwnedSubjectParameter(), Throws.TypeOf()); + // Single IUsage wired via the public API → returned. var owningType = new Type(); var subjectUsage = new Usage(); @@ -52,26 +54,36 @@ public void VerifyComputeOwnedSubjectParameter() Assert.That(subjectMembership.ComputeOwnedSubjectParameter(), Is.SameAs(subjectUsage)); - // Wiring two usages to verify the multiple-element guard: - // First create a fresh membership with two elements via the backdoor. - var twoElementMembership = new SubjectMembership(); + // Two IUsages in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException. + var twoUsageMembership = new SubjectMembership(); + var firstUsage = new Usage(); var secondUsage = new Usage(); - ((IContainedRelationship)twoElementMembership).OwnedRelatedElement.Add(subjectUsage); - ((IContainedRelationship)twoElementMembership).OwnedRelatedElement.Add(secondUsage); + ((IContainedRelationship)twoUsageMembership).OwnedRelatedElement.Add(firstUsage); + ((IContainedRelationship)twoUsageMembership).OwnedRelatedElement.Add(secondUsage); - Assert.That(() => twoElementMembership.ComputeOwnedSubjectParameter(), Throws.TypeOf()); + Assert.That(() => twoUsageMembership.ComputeOwnedSubjectParameter(), Throws.TypeOf()); - // NOTE: wiring a non-IUsage element as the sole OwnedRelatedElement is not possible via the - // public AssignOwnership API (ISubjectMembership requires an IUsage target). - // To cover the as-cast-returns-null path we directly populate OwnedRelatedElement with a - // plain Namespace (which is not an IUsage). + // Mixed-type owned related elements: exactly one IUsage alongside a non-IUsage (Namespace). + // The OfType() projection MUST pick out the IUsage regardless of its position + // (this is the core robustness guarantee — never positionally index the unfiltered collection). + var mixedMembership = new SubjectMembership(); + var siblingNonUsage = new Namespace(); + var mixedUsage = new Usage(); + + ((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(siblingNonUsage); + ((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(mixedUsage); + + Assert.That(mixedMembership.ComputeOwnedSubjectParameter(), Is.SameAs(mixedUsage)); + + // OwnedRelatedElement populated with non-IUsage element(s) only → no IUsage match: + // [1..1] violation, throws IncompleteModelException. var nonUsageMembership = new SubjectMembership(); var nonUsageElement = new Namespace(); ((IContainedRelationship)nonUsageMembership).OwnedRelatedElement.Add(nonUsageElement); - Assert.That(nonUsageMembership.ComputeOwnedSubjectParameter(), Is.Null); + Assert.That(() => nonUsageMembership.ComputeOwnedSubjectParameter(), Throws.TypeOf()); } } } diff --git a/SysML2.NET/Extend/FeatureMembershipExtensions.cs b/SysML2.NET/Extend/FeatureMembershipExtensions.cs index 05482a89..fb0cc9e1 100644 --- a/SysML2.NET/Extend/FeatureMembershipExtensions.cs +++ b/SysML2.NET/Extend/FeatureMembershipExtensions.cs @@ -22,6 +22,7 @@ namespace SysML2.NET.Core.POCO.Core.Types { using System; using System.Collections.Generic; + using System.Linq; using SysML2.NET.Core.Root.Namespaces; using SysML2.NET.Core.POCO.Core.Features; @@ -52,9 +53,11 @@ internal static IFeature ComputeOwnedMemberFeature(this IFeatureMembership featu throw new ArgumentNullException(nameof(featureMembershipSubject)); } - return featureMembershipSubject.OwnedRelatedElement.Count != 1 - ? throw new IncompleteModelException($"{nameof(featureMembershipSubject)} must have exactly one related element") - : featureMembershipSubject.OwnedRelatedElement[0] as IFeature; + var matches = featureMembershipSubject.OwnedRelatedElement.OfType().ToList(); + + return matches.Count == 1 + ? matches[0] + : throw new IncompleteModelException($"{nameof(featureMembershipSubject)} must have exactly one related element of type {nameof(IFeature)}"); } /// diff --git a/SysML2.NET/Extend/FeatureValueExtensions.cs b/SysML2.NET/Extend/FeatureValueExtensions.cs index c43c10f9..5c7d63c8 100644 --- a/SysML2.NET/Extend/FeatureValueExtensions.cs +++ b/SysML2.NET/Extend/FeatureValueExtensions.cs @@ -22,6 +22,7 @@ namespace SysML2.NET.Core.POCO.Kernel.FeatureValues { using System; using System.Collections.Generic; + using System.Linq; using SysML2.NET.Core.Root.Namespaces; using SysML2.NET.Core.POCO.Core.Features; @@ -29,6 +30,7 @@ namespace SysML2.NET.Core.POCO.Kernel.FeatureValues using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Exceptions; /// /// The class provides extensions methods for @@ -68,9 +70,11 @@ internal static IExpression ComputeValue(this IFeatureValue featureValueSubject) throw new ArgumentNullException(nameof(featureValueSubject)); } - return featureValueSubject.OwnedRelatedElement.Count == 1 - ? featureValueSubject.OwnedRelatedElement[0] as IExpression - : null; + var matches = featureValueSubject.OwnedRelatedElement.OfType().ToList(); + + return matches.Count == 1 + ? matches[0] + : throw new IncompleteModelException($"{nameof(featureValueSubject)} must have exactly one related element of type {nameof(IExpression)}"); } } } diff --git a/SysML2.NET/Extend/ParameterMembershipExtensions.cs b/SysML2.NET/Extend/ParameterMembershipExtensions.cs index 273b0f22..0b0e6705 100644 --- a/SysML2.NET/Extend/ParameterMembershipExtensions.cs +++ b/SysML2.NET/Extend/ParameterMembershipExtensions.cs @@ -22,6 +22,7 @@ namespace SysML2.NET.Core.POCO.Kernel.Behaviors { using System; using System.Collections.Generic; + using System.Linq; using SysML2.NET.Core.Core.Types; using SysML2.NET.Core.Root.Namespaces; @@ -54,9 +55,11 @@ internal static IFeature ComputeOwnedMemberParameter(this IParameterMembership p throw new ArgumentNullException(nameof(parameterMembershipSubject)); } - return parameterMembershipSubject.OwnedRelatedElement.Count != 1 - ? throw new IncompleteModelException($"{nameof(parameterMembershipSubject)} must have exactly one related element") - : parameterMembershipSubject.OwnedRelatedElement[0] as IFeature; + var matches = parameterMembershipSubject.OwnedRelatedElement.OfType().ToList(); + + return matches.Count == 1 + ? matches[0] + : throw new IncompleteModelException($"{nameof(parameterMembershipSubject)} must have exactly one related element of type {nameof(IFeature)}"); } /// diff --git a/SysML2.NET/Extend/SubjectMembershipExtensions.cs b/SysML2.NET/Extend/SubjectMembershipExtensions.cs index d3841ee6..8b10bec8 100644 --- a/SysML2.NET/Extend/SubjectMembershipExtensions.cs +++ b/SysML2.NET/Extend/SubjectMembershipExtensions.cs @@ -21,6 +21,8 @@ namespace SysML2.NET.Core.POCO.Systems.Requirements { using System; + using System.Collections.Generic; + using System.Linq; using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; using SysML2.NET.Exceptions; @@ -47,9 +49,11 @@ internal static IUsage ComputeOwnedSubjectParameter(this ISubjectMembership subj throw new ArgumentNullException(nameof(subjectMembershipSubject)); } - return subjectMembershipSubject.OwnedRelatedElement.Count != 1 - ? throw new IncompleteModelException($"{nameof(subjectMembershipSubject)} must have exactly one related element") - : subjectMembershipSubject.OwnedRelatedElement[0] as IUsage; + var matches = subjectMembershipSubject.OwnedRelatedElement.OfType().ToList(); + + return matches.Count == 1 + ? matches[0] + : throw new IncompleteModelException($"{nameof(subjectMembershipSubject)} must have exactly one related element of type {nameof(IUsage)}"); } }