diff --git a/CLAUDE.md b/CLAUDE.md index 22679c85..36fb6f58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,6 +155,7 @@ Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package - Prefer 'string.IsNullOrWhiteSpace' over 'string.IsNullOrEmpty' when checking the non-nullable value of a string - Prefer switch expressions/statements over if-else chains when applicable - Prefer indexer syntax (e.g., 'list[^1]') and range syntax (e.g., 'array[1..^1]') over LINQ methods (e.g., 'list.Last()', 'list.Skip(1).Take(n)') when applicable +- Prefer C# collection expressions (`[a, b, c]`, `[..xs]`, `[]`) over `new[] { ... }`, `new List { ... }`, `new T[] { ... }` when constructing a collection. Applies to both production code AND tests (e.g. `Is.EqualTo([classifier1, classifier2])` not `Is.EqualTo(new[] { classifier1, classifier2 })`, `return [];` not `return new List();`). Fall back to explicit construction only when type inference cannot pick the right collection type. - Use meaningful variable names instead of single-letter names in any context (e.g., 'charIndex' instead of 'i', 'currentChar' instead of 'c', 'element' instead of 'e') - Use 'NotSupportedException' (not 'NotImplementedException') for placeholder/stub methods that require manual implementation - 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 diff --git a/SysML2.NET.Tests/Extend/FlowExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FlowExtensionsTestFixture.cs index 344b82db..02b2ade4 100644 --- a/SysML2.NET.Tests/Extend/FlowExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/FlowExtensionsTestFixture.cs @@ -1,68 +1,275 @@ // ------------------------------------------------------------------------------------------------- // -// +// // 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.Classifiers; + using SysML2.NET.Core.POCO.Core.Features; + using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Kernel.Interactions; + using SysML2.NET.Extensions; + + using PocoType = SysML2.NET.Core.POCO.Core.Types.Type; [TestFixture] public class FlowExtensionsTestFixture { [Test] - public void ComputeFlowEnd_ThrowsNotSupportedException() + public void VerifyComputeFlowEnd() { - Assert.That(() => ((IFlow)null).ComputeFlowEnd(), Throws.TypeOf()); + Assert.That(() => ((IFlow)null).ComputeFlowEnd(), Throws.TypeOf()); + + var flow = new Flow(); + + // Empty: no connectorEnds → empty list. + Assert.That(flow.ComputeFlowEnd(), Is.Empty); + + // Discrimination: a plain Feature (IsEnd=true) wired via FeatureMembership is NOT a FlowEnd → excluded. + var plainEndFeature = new Feature { IsEnd = true }; + var plainEndMembership = new FeatureMembership(); + flow.AssignOwnership(plainEndMembership, plainEndFeature); + + Assert.That(flow.ComputeFlowEnd(), Is.Empty); + + // Positive: FlowEnd with IsEnd=true → returned. + var flowEnd1 = new FlowEnd { IsEnd = true }; + var flowEndMembership1 = new FeatureMembership(); + flow.AssignOwnership(flowEndMembership1, flowEnd1); + + Assert.That(flow.ComputeFlowEnd(), Is.EqualTo([flowEnd1])); + + // Populated: second FlowEnd also returned, in iteration order. + var flowEnd2 = new FlowEnd { IsEnd = true }; + var flowEndMembership2 = new FeatureMembership(); + flow.AssignOwnership(flowEndMembership2, flowEnd2); + + Assert.That(flow.ComputeFlowEnd(), Is.EqualTo([flowEnd1, flowEnd2])); } - + [Test] - public void ComputeInteraction_ThrowsNotSupportedException() + public void VerifyComputeInteraction() { - Assert.That(() => ((IFlow)null).ComputeInteraction(), Throws.TypeOf()); + Assert.That(() => ((IFlow)null).ComputeInteraction(), Throws.TypeOf()); + + var flow = new Flow(); + + // Empty: no OwnedRelationship → empty list. + Assert.That(flow.ComputeInteraction(), Is.Empty); + + // Discrimination: FeatureTyping pointing at a non-Interaction Type → excluded. + var nonInteractionType = new PocoType(); + var typingToNonInteraction = new FeatureTyping { Type = nonInteractionType }; + flow.AssignOwnership(typingToNonInteraction); + + Assert.That(flow.ComputeInteraction(), Is.Empty); + + // Positive: FeatureTyping pointing at an Interaction → returned. + var interaction1 = new Interaction(); + var typingToInteraction1 = new FeatureTyping { Type = interaction1 }; + flow.AssignOwnership(typingToInteraction1); + + Assert.That(flow.ComputeInteraction(), Is.EqualTo([interaction1])); + + // Populated: second Interaction also returned, in iteration order. + var interaction2 = new Interaction(); + var typingToInteraction2 = new FeatureTyping { Type = interaction2 }; + flow.AssignOwnership(typingToInteraction2); + + Assert.That(flow.ComputeInteraction(), Is.EqualTo([interaction1, interaction2])); } - + [Test] - public void ComputePayloadFeature_ThrowsNotSupportedException() + public void VerifyComputePayloadFeature() { - Assert.That(() => ((IFlow)null).ComputePayloadFeature(), Throws.TypeOf()); + Assert.That(() => ((IFlow)null).ComputePayloadFeature(), Throws.TypeOf()); + + var flow = new Flow(); + + // Empty: no ownedFeature → null. + Assert.That(flow.ComputePayloadFeature(), Is.Null); + + // Discrimination: plain Feature (not PayloadFeature) wired via FeatureMembership → null. + var plainFeature = new Feature(); + var plainFeatureMembership = new FeatureMembership(); + flow.AssignOwnership(plainFeatureMembership, plainFeature); + + Assert.That(flow.ComputePayloadFeature(), Is.Null); + + // Positive: PayloadFeature wired via FeatureMembership → returned. + var payloadFeature1 = new PayloadFeature(); + var payloadMembership1 = new FeatureMembership(); + flow.AssignOwnership(payloadMembership1, payloadFeature1); + + Assert.That(flow.ComputePayloadFeature(), Is.SameAs(payloadFeature1)); + + // Multiple PayloadFeatures → first one returned (index 0). + var payloadFeature2 = new PayloadFeature(); + var payloadMembership2 = new FeatureMembership(); + flow.AssignOwnership(payloadMembership2, payloadFeature2); + + Assert.That(flow.ComputePayloadFeature(), Is.SameAs(payloadFeature1)); } - + [Test] - public void ComputePayloadType_ThrowsNotSupportedException() + public void VerifyComputePayloadType() { - Assert.That(() => ((IFlow)null).ComputePayloadType(), Throws.TypeOf()); + Assert.That(() => ((IFlow)null).ComputePayloadType(), Throws.TypeOf()); + + var flow = new Flow(); + + // Empty: no PayloadFeature → empty list (short-circuits). + Assert.That(flow.ComputePayloadType(), Is.Empty); + + // Wire a PayloadFeature with no types → empty list. + var payloadFeatureEmpty = new PayloadFeature(); + var emptyPayloadMembership = new FeatureMembership(); + flow.AssignOwnership(emptyPayloadMembership, payloadFeatureEmpty); + + Assert.That(flow.ComputePayloadType(), Is.Empty); + + // Build a fresh flow for the discrimination and positive cases. + var flow2 = new Flow(); + var payloadFeature = new PayloadFeature(); + var payloadMembership = new FeatureMembership(); + flow2.AssignOwnership(payloadMembership, payloadFeature); + + // Discrimination: non-Classifier IType in payload feature type → excluded. + var nonClassifierType = new PocoType(); + var typingToNonClassifier = new FeatureTyping { Type = nonClassifierType }; + payloadFeature.AssignOwnership(typingToNonClassifier); + + Assert.That(flow2.ComputePayloadType(), Is.Empty); + + // Positive: Classifier type in payload feature → returned. + var classifier1 = new Classifier(); + var typingToClassifier1 = new FeatureTyping { Type = classifier1 }; + payloadFeature.AssignOwnership(typingToClassifier1); + + Assert.That(flow2.ComputePayloadType(), Is.EqualTo([classifier1])); + + // Multiple Classifier types → all returned. + var classifier2 = new Classifier(); + var typingToClassifier2 = new FeatureTyping { Type = classifier2 }; + payloadFeature.AssignOwnership(typingToClassifier2); + + Assert.That(flow2.ComputePayloadType(), Is.EqualTo([classifier1, classifier2])); } - + [Test] - public void ComputeSourceOutputFeature_ThrowsNotSupportedException() + public void VerifyComputeSourceOutputFeature() { - Assert.That(() => ((IFlow)null).ComputeSourceOutputFeature(), Throws.TypeOf()); + Assert.That(() => ((IFlow)null).ComputeSourceOutputFeature(), Throws.TypeOf()); + + var flow = new Flow(); + + // Empty connectorEnd → null. + Assert.That(flow.ComputeSourceOutputFeature(), Is.Null); + + // One connectorEnd with no ownedFeature → flat sequence empty → null. + var end1 = new Feature { IsEnd = true }; + var endMembership1 = new FeatureMembership(); + flow.AssignOwnership(endMembership1, end1); + + Assert.That(flow.ComputeSourceOutputFeature(), Is.Null); + + // One connectorEnd with one ownedFeature → that feature returned. + var innerFeatureA = new Feature(); + var innerMembershipA = new FeatureMembership(); + end1.AssignOwnership(innerMembershipA, innerFeatureA); + + Assert.That(flow.ComputeSourceOutputFeature(), Is.SameAs(innerFeatureA)); + + // Two connectorEnds (each with one ownedFeature) → first end's first ownedFeature returned. + var end2 = new Feature { IsEnd = true }; + var endMembership2 = new FeatureMembership(); + flow.AssignOwnership(endMembership2, end2); + + var innerFeatureB = new Feature(); + var innerMembershipB = new FeatureMembership(); + end2.AssignOwnership(innerMembershipB, innerFeatureB); + + Assert.That(flow.ComputeSourceOutputFeature(), Is.SameAs(innerFeatureA)); + + // Empty first end + non-empty second end → second end's first feature returned. + var flow2 = new Flow(); + + var emptyEnd = new Feature { IsEnd = true }; + var emptyEndMembership = new FeatureMembership(); + flow2.AssignOwnership(emptyEndMembership, emptyEnd); + + var nonEmptyEnd = new Feature { IsEnd = true }; + var nonEmptyEndMembership = new FeatureMembership(); + flow2.AssignOwnership(nonEmptyEndMembership, nonEmptyEnd); + + var secondEndFeature = new Feature(); + var secondEndInnerMembership = new FeatureMembership(); + nonEmptyEnd.AssignOwnership(secondEndInnerMembership, secondEndFeature); + + Assert.That(flow2.ComputeSourceOutputFeature(), Is.SameAs(secondEndFeature)); } - + [Test] - public void ComputeTargetInputFeature_ThrowsNotSupportedException() + public void VerifyComputeTargetInputFeature() { - Assert.That(() => ((IFlow)null).ComputeTargetInputFeature(), Throws.TypeOf()); + Assert.That(() => ((IFlow)null).ComputeTargetInputFeature(), Throws.TypeOf()); + + var flow = new Flow(); + + // Zero connectorEnds → Count < 2 → null. + Assert.That(flow.ComputeTargetInputFeature(), Is.Null); + + // One connectorEnd → Count == 1 < 2 → null. + var end1 = new Feature { IsEnd = true }; + var endMembership1 = new FeatureMembership(); + flow.AssignOwnership(endMembership1, end1); + + Assert.That(flow.ComputeTargetInputFeature(), Is.Null); + + // Two connectorEnds, second has empty ownedFeature → null. + var end2 = new Feature { IsEnd = true }; + var endMembership2 = new FeatureMembership(); + flow.AssignOwnership(endMembership2, end2); + + Assert.That(flow.ComputeTargetInputFeature(), Is.Null); + + // Two connectorEnds, second has one ownedFeature → that feature returned. + var targetFeature = new Feature(); + var targetMembership = new FeatureMembership(); + end2.AssignOwnership(targetMembership, targetFeature); + + Assert.That(flow.ComputeTargetInputFeature(), Is.SameAs(targetFeature)); + + // Three connectorEnds — still returns second end's feature (NOT third end). + var end3 = new Feature { IsEnd = true }; + var endMembership3 = new FeatureMembership(); + flow.AssignOwnership(endMembership3, end3); + + var thirdEndFeature = new Feature(); + var thirdEndMembership = new FeatureMembership(); + end3.AssignOwnership(thirdEndMembership, thirdEndFeature); + + Assert.That(flow.ComputeTargetInputFeature(), Is.SameAs(targetFeature)); } } } diff --git a/SysML2.NET/Extend/FlowExtensions.cs b/SysML2.NET/Extend/FlowExtensions.cs index 539566fb..c0e10983 100644 --- a/SysML2.NET/Extend/FlowExtensions.cs +++ b/SysML2.NET/Extend/FlowExtensions.cs @@ -22,18 +22,14 @@ namespace SysML2.NET.Core.POCO.Kernel.Interactions { 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.Classifiers; using SysML2.NET.Core.POCO.Core.Features; using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Kernel.Associations; using SysML2.NET.Core.POCO.Kernel.Behaviors; using SysML2.NET.Core.POCO.Kernel.Connectors; - using SysML2.NET.Core.POCO.Root.Annotations; - using SysML2.NET.Core.POCO.Root.Elements; - using SysML2.NET.Core.POCO.Root.Namespaces; /// /// The class provides extensions methods for @@ -56,10 +52,11 @@ internal static class FlowExtensions /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static List ComputeFlowEnd(this IFlow flowSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + return flowSubject == null + ? throw new ArgumentNullException(nameof(flowSubject)) + : [..flowSubject.connectorEnd.OfType()]; } /// @@ -71,10 +68,11 @@ internal static List ComputeFlowEnd(this IFlow flowSubject) /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static List ComputeInteraction(this IFlow flowSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + return flowSubject == null + ? throw new ArgumentNullException(nameof(flowSubject)) + : [..flowSubject.OwnedRelationship.OfType().Select(featureTyping => featureTyping.Type).OfType()]; } /// @@ -97,10 +95,16 @@ internal static List ComputeInteraction(this IFlow flowSubject) /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IPayloadFeature ComputePayloadFeature(this IFlow flowSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (flowSubject == null) + { + throw new ArgumentNullException(nameof(flowSubject)); + } + + var payloadFeatures = flowSubject.ownedFeature.OfType().ToList(); + + return payloadFeatures.Count == 0 ? null : payloadFeatures[0]; } /// @@ -121,10 +125,18 @@ internal static IPayloadFeature ComputePayloadFeature(this IFlow flowSubject) /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static List ComputePayloadType(this IFlow flowSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (flowSubject == null) + { + throw new ArgumentNullException(nameof(flowSubject)); + } + + var payloadFeature = flowSubject.payloadFeature; + + return payloadFeature == null + ? [] + : [..payloadFeature.type.OfType()]; } /// @@ -147,10 +159,23 @@ internal static List ComputePayloadType(this IFlow flowSubject) /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IFeature ComputeSourceOutputFeature(this IFlow flowSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (flowSubject == null) + { + throw new ArgumentNullException(nameof(flowSubject)); + } + + var connectorEnds = flowSubject.connectorEnd; + + if (connectorEnds.Count == 0) + { + return null; + } + + var flatOwnedFeatures = connectorEnds.SelectMany(connectorEndFeature => connectorEndFeature.ownedFeature).ToList(); + + return flatOwnedFeatures.Count == 0 ? null : flatOwnedFeatures[0]; } /// @@ -173,11 +198,23 @@ internal static IFeature ComputeSourceOutputFeature(this IFlow flowSubject) /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IFeature ComputeTargetInputFeature(this IFlow flowSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); - } + if (flowSubject == null) + { + throw new ArgumentNullException(nameof(flowSubject)); + } + + var connectorEnds = flowSubject.connectorEnd; + if (connectorEnds.Count < 2) + { + return null; + } + + var secondConnectorEndOwnedFeatures = connectorEnds[1].ownedFeature; + + return secondConnectorEndOwnedFeatures.Count == 0 ? null : secondConnectorEndOwnedFeatures[0]; + } } }