diff --git a/SysML2.NET.Tests/Extend/MultiplicityRangeExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/MultiplicityRangeExtensionsTestFixture.cs index 01b99270..b8a04b49 100644 --- a/SysML2.NET.Tests/Extend/MultiplicityRangeExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/MultiplicityRangeExtensionsTestFixture.cs @@ -1,50 +1,285 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // -// +// // 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 System.Collections.Generic; + + using Moq; + using NUnit.Framework; - + + using SysML2.NET.Core.POCO.Kernel.Expressions; + using SysML2.NET.Core.POCO.Kernel.Functions; using SysML2.NET.Core.POCO.Kernel.Multiplicities; + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; + using SysML2.NET.Extensions; [TestFixture] public class MultiplicityRangeExtensionsTestFixture { [Test] - public void ComputeBound_ThrowsNotSupportedException() + public void VerifyComputeBound() { - Assert.That(() => ((IMultiplicityRange)null).ComputeBound(), Throws.TypeOf()); + Assert.That(() => ((IMultiplicityRange)null).ComputeBound(), Throws.TypeOf()); + + var multiplicityRange = new MultiplicityRange(); + + Assert.That(multiplicityRange.ComputeBound(), Has.Count.EqualTo(0)); + + var firstExpression = new Expression(); + multiplicityRange.AssignOwnership(new OwningMembership(), firstExpression); + + using (Assert.EnterMultipleScope()) + { + // Exactly one Expression: bound == [upperBound] (lowerBound is null). + Assert.That(multiplicityRange.ComputeBound(), Is.EquivalentTo([firstExpression])); + Assert.That(multiplicityRange.ComputeBound()[0], Is.SameAs(multiplicityRange.upperBound)); + } + + var secondExpression = new Expression(); + multiplicityRange.AssignOwnership(new OwningMembership(), secondExpression); + + using (Assert.EnterMultipleScope()) + { + // Two Expressions: bound == [lowerBound, upperBound] in order. + Assert.That(multiplicityRange.ComputeBound(), Is.EqualTo([firstExpression, secondExpression])); + Assert.That(multiplicityRange.ComputeBound()[0], Is.SameAs(multiplicityRange.lowerBound)); + Assert.That(multiplicityRange.ComputeBound()[1], Is.SameAs(multiplicityRange.upperBound)); + } + + // Mixed: a non-Expression owned member preceding two Expressions; OfType() filters it out + // so the bound list is still [firstExpression, secondExpression]. + var mixedRange = new MultiplicityRange(); + var nonExpressionElement = new Definition(); + mixedRange.AssignOwnership(new OwningMembership(), nonExpressionElement); + + var mixedLower = new Expression(); + var mixedUpper = new Expression(); + mixedRange.AssignOwnership(new OwningMembership(), mixedLower); + mixedRange.AssignOwnership(new OwningMembership(), mixedUpper); + + Assert.That(mixedRange.ComputeBound(), Is.EqualTo([mixedLower, mixedUpper])); + } + + [Test] + public void VerifyComputeLowerBound() + { + Assert.That(() => ((IMultiplicityRange)null).ComputeLowerBound(), Throws.TypeOf()); + + var multiplicityRange = new MultiplicityRange(); + + Assert.That(multiplicityRange.ComputeLowerBound(), Is.Null); + + var firstExpression = new Expression(); + multiplicityRange.AssignOwnership(new OwningMembership(), firstExpression); + + // Exactly one Expression -> lowerBound is null (the single expression is the upperBound). + Assert.That(multiplicityRange.ComputeLowerBound(), Is.Null); + + var secondExpression = new Expression(); + multiplicityRange.AssignOwnership(new OwningMembership(), secondExpression); + + // Two Expressions -> lowerBound is the first one. + Assert.That(multiplicityRange.ComputeLowerBound(), Is.SameAs(firstExpression)); + + // Mixed: a non-Expression ownedMember preceding two Expressions; OfType() filters it out. + var mixedRange = new MultiplicityRange(); + var nonExpressionElement = new Definition(); + mixedRange.AssignOwnership(new OwningMembership(), nonExpressionElement); + + var mixedLower = new Expression(); + var mixedUpper = new Expression(); + mixedRange.AssignOwnership(new OwningMembership(), mixedLower); + mixedRange.AssignOwnership(new OwningMembership(), mixedUpper); + + Assert.That(mixedRange.ComputeLowerBound(), Is.SameAs(mixedLower)); + } + + [Test] + public void VerifyComputeUpperBound() + { + Assert.That(() => ((IMultiplicityRange)null).ComputeUpperBound(), Throws.TypeOf()); + + var multiplicityRange = new MultiplicityRange(); + + Assert.That(multiplicityRange.ComputeUpperBound(), Is.Null); + + var firstExpression = new Expression(); + multiplicityRange.AssignOwnership(new OwningMembership(), firstExpression); + + // Exactly one Expression -> upperBound IS that expression. + Assert.That(multiplicityRange.ComputeUpperBound(), Is.SameAs(firstExpression)); + + var secondExpression = new Expression(); + multiplicityRange.AssignOwnership(new OwningMembership(), secondExpression); + + using (Assert.EnterMultipleScope()) + { + // Two Expressions -> upperBound is the second one (OCL at(2), C# index 1). + Assert.That(multiplicityRange.ComputeUpperBound(), Is.SameAs(secondExpression)); + Assert.That(multiplicityRange.ComputeUpperBound(), Is.Not.SameAs(firstExpression)); + } + + // Mixed: a non-Expression ownedMember preceding two Expressions; the second Expression is the upperBound. + var mixedRange = new MultiplicityRange(); + var nonExpressionElement = new Definition(); + mixedRange.AssignOwnership(new OwningMembership(), nonExpressionElement); + + var mixedLower = new Expression(); + var mixedUpper = new Expression(); + mixedRange.AssignOwnership(new OwningMembership(), mixedLower); + mixedRange.AssignOwnership(new OwningMembership(), mixedUpper); + + Assert.That(mixedRange.ComputeUpperBound(), Is.SameAs(mixedUpper)); } - + [Test] - public void ComputeLowerBound_ThrowsNotSupportedException() + public void VerifyComputeHasBoundsOperation() { - Assert.That(() => ((IMultiplicityRange)null).ComputeLowerBound(), Throws.TypeOf()); + Assert.That(() => ((IMultiplicityRange)null).ComputeHasBoundsOperation(0, "*"), Throws.TypeOf()); + + // Case A: lowerBound evaluates to "1", upperBound evaluates to "5". + // HasBounds(1, "5") -> true (both match exactly). + // HasBounds(0, "5") -> false (upper matches but lower does not, and lowerValue is not null). + // HasBounds(1, "6") -> false (upper does not match). + var rangeOneFive = BuildMockedRange(lowerValueOf: "1", upperValueOf: "5"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(rangeOneFive.ComputeHasBoundsOperation(1, "5"), Is.True); + Assert.That(rangeOneFive.ComputeHasBoundsOperation(0, "5"), Is.False); + Assert.That(rangeOneFive.ComputeHasBoundsOperation(1, "6"), Is.False); + } + + // Case B: no lowerBound (lowerValue == null), upperBound evaluates to "*". + // HasBounds(0, "*") -> true via the 0..* implied branch. + // HasBounds(1, "*") -> false (lower != upper and not the 0..* shape). + var rangeZeroStar = BuildMockedRange(lowerValueOf: null, upperValueOf: "*"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(rangeZeroStar.ComputeHasBoundsOperation(0, "*"), Is.True); + Assert.That(rangeZeroStar.ComputeHasBoundsOperation(1, "*"), Is.False); + } + + // Case C: no lowerBound (lowerValue == null), upperBound evaluates to "3". + // HasBounds(3, "3") -> true via the single-multiplicity implied branch (lower == upper). + // HasBounds(0, "3") -> false (neither branch satisfied). + var rangeThree = BuildMockedRange(lowerValueOf: null, upperValueOf: "3"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(rangeThree.ComputeHasBoundsOperation(3, "3"), Is.True); + Assert.That(rangeThree.ComputeHasBoundsOperation(0, "3"), Is.False); + } } - + [Test] - public void ComputeUpperBound_ThrowsNotSupportedException() + public void VerifyComputeValueOfOperation() { - Assert.That(() => ((IMultiplicityRange)null).ComputeUpperBound(), Throws.TypeOf()); + var multiplicityRange = new MultiplicityRange(); + + Assert.That(() => ((IMultiplicityRange)null).ComputeValueOfOperation(null), Throws.TypeOf()); + + // bound == null -> null (explicit OCL contract, NOT a throw). + Assert.That(multiplicityRange.ComputeValueOfOperation(null), Is.Null); + + // bound.isModelLevelEvaluable == false -> null. + var notEvaluable = new Mock(); + notEvaluable.Setup(x => x.isModelLevelEvaluable).Returns(false); + + Assert.That(multiplicityRange.ComputeValueOfOperation(notEvaluable.Object), Is.Null); + + // Evaluate returns [] -> null. + Assert.That(multiplicityRange.ComputeValueOfOperation(BuildEvaluableExpression([])), Is.Null); + + // Evaluate returns two elements -> null. + var twoElementResult = BuildEvaluableExpression( + [new LiteralInteger { Value = 1 }, new LiteralInteger { Value = 2 }]); + Assert.That(multiplicityRange.ComputeValueOfOperation(twoElementResult), Is.Null); + + // Evaluate returns [LiteralInfinity] -> "*". + var infinity = BuildEvaluableExpression([new LiteralInfinity()]); + Assert.That(multiplicityRange.ComputeValueOfOperation(infinity), Is.EqualTo("*")); + + // Evaluate returns [LiteralInteger { Value = 0 }] -> "0". + var zero = BuildEvaluableExpression([new LiteralInteger { Value = 0 }]); + Assert.That(multiplicityRange.ComputeValueOfOperation(zero), Is.EqualTo("0")); + + // Evaluate returns [LiteralInteger { Value = 5 }] -> "5". + var five = BuildEvaluableExpression([new LiteralInteger { Value = 5 }]); + Assert.That(multiplicityRange.ComputeValueOfOperation(five), Is.EqualTo("5")); + + // Evaluate returns [LiteralInteger { Value = -1 }] -> null (negative not representable as UnlimitedNatural). + var negative = BuildEvaluableExpression([new LiteralInteger { Value = -1 }]); + Assert.That(multiplicityRange.ComputeValueOfOperation(negative), Is.Null); + + // Evaluate returns [LiteralBoolean] -> null (not a LiteralInfinity / LiteralInteger). + var boolean = BuildEvaluableExpression([new LiteralBoolean { Value = true }]); + Assert.That(multiplicityRange.ComputeValueOfOperation(boolean), Is.Null); + } + + /// + /// Builds a Moq-backed whose isModelLevelEvaluable is true + /// and whose Evaluate returns the supplied result list for any target. + /// + /// The list of that Evaluate should yield. + /// The configured mock . + private static IExpression BuildEvaluableExpression(List result) + { + var mockExpression = new Mock(); + mockExpression.Setup(x => x.isModelLevelEvaluable).Returns(true); + mockExpression.Setup(x => x.Evaluate(It.IsAny())).Returns(result); + + return mockExpression.Object; + } + + /// + /// Builds a Moq-backed whose ValueOf(upperBound) returns + /// and whose ValueOf(lowerBound) returns . + /// When is null the mock's lowerBound property returns null, exercising + /// the "no lower bound" branches of ComputeHasBoundsOperation. + /// + /// + /// The string the mock's ValueOf(lowerBound) should return; or null if the range has no lowerBound. + /// + /// + /// The string the mock's ValueOf(upperBound) should return. + /// + /// The configured mock . + private static IMultiplicityRange BuildMockedRange(string lowerValueOf, string upperValueOf) + { + var upperBoundExpression = new Mock().Object; + var lowerBoundExpression = lowerValueOf is null ? null : new Mock().Object; + + var mockRange = new Mock(); + mockRange.Setup(x => x.upperBound).Returns(upperBoundExpression); + mockRange.Setup(x => x.lowerBound).Returns(lowerBoundExpression); + mockRange.Setup(x => x.ValueOf(upperBoundExpression)).Returns(upperValueOf); + mockRange.Setup(x => x.ValueOf(lowerBoundExpression)).Returns(lowerValueOf); + + return mockRange.Object; } } } diff --git a/SysML2.NET/Extend/MultiplicityRangeExtensions.cs b/SysML2.NET/Extend/MultiplicityRangeExtensions.cs index a525e357..9a43a136 100644 --- a/SysML2.NET/Extend/MultiplicityRangeExtensions.cs +++ b/SysML2.NET/Extend/MultiplicityRangeExtensions.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // // // Copyright (C) 2022-2026 Starion Group S.A. @@ -22,15 +22,11 @@ namespace SysML2.NET.Core.POCO.Kernel.Multiplicities { using System; using System.Collections.Generic; + using System.Globalization; + 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.Kernel.Expressions; using SysML2.NET.Core.POCO.Kernel.Functions; - 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 @@ -57,10 +53,23 @@ internal static class MultiplicityRangeExtensions /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static List ComputeBound(this IMultiplicityRange multiplicityRangeSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (multiplicityRangeSubject is null) + { + throw new ArgumentNullException(nameof(multiplicityRangeSubject)); + } + + var upper = multiplicityRangeSubject.upperBound; + + if (upper is null) + { + return []; + } + + var lower = multiplicityRangeSubject.lowerBound; + + return lower is null ? [upper] : [lower, upper]; } /// @@ -83,10 +92,18 @@ internal static List ComputeBound(this IMultiplicityRange multiplic /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IExpression ComputeLowerBound(this IMultiplicityRange multiplicityRangeSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (multiplicityRangeSubject is null) + { + throw new ArgumentNullException(nameof(multiplicityRangeSubject)); + } + + var ownedExpressions = multiplicityRangeSubject.ownedMember + .OfType() + .ToList(); + + return ownedExpressions.Count < 2 ? null : ownedExpressions[0]; } /// @@ -110,10 +127,23 @@ internal static IExpression ComputeLowerBound(this IMultiplicityRange multiplici /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IExpression ComputeUpperBound(this IMultiplicityRange multiplicityRangeSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (multiplicityRangeSubject is null) + { + throw new ArgumentNullException(nameof(multiplicityRangeSubject)); + } + + var ownedExpressions = multiplicityRangeSubject.ownedMember + .OfType() + .ToList(); + + return ownedExpressions.Count switch + { + 0 => null, + 1 => ownedExpressions[0], + _ => ownedExpressions[1], + }; } /// @@ -135,18 +165,43 @@ internal static IExpression ComputeUpperBound(this IMultiplicityRange multiplici /// The subject /// /// - /// No documentation provided + /// The candidate lower bound as a non-negative integer. /// /// - /// No documentation provided + /// The candidate upper bound encoded as an UnlimitedNatural string ("*" for unbounded, or an invariant-culture decimal). /// /// /// The expected /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static bool ComputeHasBoundsOperation(this IMultiplicityRange multiplicityRangeSubject, int lower, string upper) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (multiplicityRangeSubject is null) + { + throw new ArgumentNullException(nameof(multiplicityRangeSubject)); + } + + var valueOfUpper = multiplicityRangeSubject.ValueOf(multiplicityRangeSubject.upperBound); + + if (!string.Equals(valueOfUpper, upper, StringComparison.Ordinal)) + { + return false; + } + + var lowerValue = multiplicityRangeSubject.ValueOf(multiplicityRangeSubject.lowerBound); + var lowerAsString = lower.ToString(CultureInfo.InvariantCulture); + + if (string.Equals(lowerValue, lowerAsString, StringComparison.Ordinal)) + { + return true; + } + + if (lowerValue is not null) + { + return false; + } + + return string.Equals(lowerAsString, upper, StringComparison.Ordinal) + || (lower == 0 && string.Equals(upper, "*", StringComparison.Ordinal)); } /// @@ -177,15 +232,36 @@ internal static bool ComputeHasBoundsOperation(this IMultiplicityRange multiplic /// The subject /// /// - /// No documentation provided + /// The bound expression to evaluate; may be null, in which case the result is null. /// /// /// The expected /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static string ComputeValueOfOperation(this IMultiplicityRange multiplicityRangeSubject, IExpression bound) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (multiplicityRangeSubject is null) + { + throw new ArgumentNullException(nameof(multiplicityRangeSubject)); + } + + if (bound is null || !bound.isModelLevelEvaluable) + { + return null; + } + + var boundEval = bound.Evaluate(multiplicityRangeSubject.owningType); + + if (boundEval.Count != 1) + { + return null; + } + + return boundEval[0] switch + { + ILiteralInfinity => "*", + ILiteralInteger { Value: var value } when value >= 0 => value.ToString(CultureInfo.InvariantCulture), + _ => null, + }; } } }