From 40bba5fc3ec0e0a101b555594a4056bc96e7a80f Mon Sep 17 00:00:00 2001 From: atheate Date: Thu, 21 May 2026 21:45:03 +0200 Subject: [PATCH] Fix #128 --- .../Extend/ExpressionExtensionsTestFixture.cs | 269 +++++++++++++++++- SysML2.NET/Extend/ExpressionExtensions.cs | 88 ++++-- 2 files changed, 325 insertions(+), 32 deletions(-) diff --git a/SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs index cf5de798..b8fb5643 100644 --- a/SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs @@ -1,50 +1,289 @@ // ------------------------------------------------------------------------------------------------- // -// +// // 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 NUnit.Framework; - + + using SysML2.NET.Core.Core.Types; + 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.FeatureValues; using SysML2.NET.Core.POCO.Kernel.Functions; + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Extensions; [TestFixture] public class ExpressionExtensionsTestFixture { [Test] - public void ComputeFunction_ThrowsNotSupportedException() + public void VerifyComputeFunction() + { + Assert.That(() => ((IExpression)null).ComputeFunction(), Throws.TypeOf()); + + var expression = new Expression(); + + // Empty: no typings → null. + Assert.That(expression.ComputeFunction(), Is.Null); + + // Negative: FeatureTyping pointing at a non-Function Type → null. + var nonFunctionType = new SysML2.NET.Core.POCO.Core.Types.Type(); + var typingToNonFunction = new FeatureTyping { Type = nonFunctionType }; + expression.AssignOwnership(typingToNonFunction); + + Assert.That(expression.ComputeFunction(), Is.Null); + + // Positive: FeatureTyping pointing at a Function → that Function returned. + var function1 = new Function(); + var typingToFunction1 = new FeatureTyping { Type = function1 }; + expression.AssignOwnership(typingToFunction1); + + Assert.That(expression.ComputeFunction(), Is.SameAs(function1)); + + // Multiple function typings (pathological but must not throw) → first returned. + var function2 = new Function(); + var typingToFunction2 = new FeatureTyping { Type = function2 }; + expression.AssignOwnership(typingToFunction2); + + Assert.That(expression.ComputeFunction(), Is.SameAs(function1)); + } + + [Test] + public void VerifyComputeIsModelLevelEvaluable() { - Assert.That(() => ((IExpression)null).ComputeFunction(), Throws.TypeOf()); + Assert.That(() => ((IExpression)null).ComputeIsModelLevelEvaluable(), Throws.TypeOf()); + + var expression = new Expression(); + + // Empty/default: no ownedSpecialization, no ownedFeature → true (vacuous forAll). + Assert.That(expression.ComputeIsModelLevelEvaluable(), Is.True); + + // One implied specialization (IsImplied = true) → still true. + var impliedSpecialization = new Specialization { IsImplied = true, Specific = expression }; + expression.AssignOwnership(impliedSpecialization); + + Assert.That(expression.ComputeIsModelLevelEvaluable(), Is.True); + + // One non-implied specialization → false (early return from forAll). + var expressionWithNonImplied = new Expression(); + var nonImpliedSpecialization = new Specialization { IsImplied = false, Specific = expressionWithNonImplied }; + expressionWithNonImplied.AssignOwnership(nonImpliedSpecialization); + + Assert.That(expressionWithNonImplied.ComputeIsModelLevelEvaluable(), Is.False); } - + [Test] - public void ComputeIsModelLevelEvaluable_ThrowsNotSupportedException() + public void VerifyComputeResult() { - Assert.That(() => ((IExpression)null).ComputeIsModelLevelEvaluable(), Throws.TypeOf()); + Assert.That(() => ((IExpression)null).ComputeResult(), Throws.TypeOf()); + + var expression = new Expression(); + + // Empty: no featureMembership → null. + Assert.That(expression.ComputeResult(), Is.Null); + + // Negative: FeatureMembership with non-ReturnParameterMembership → null. + var featureMembership = new FeatureMembership(); + var plainFeature = new Feature(); + expression.AssignOwnership(featureMembership, plainFeature); + + Assert.That(expression.ComputeResult(), Is.Null); + + // Positive: one ReturnParameterMembership with ownedMemberParameter set → that Feature returned. + var resultFeature = new Feature(); + var returnParamMembership = new ReturnParameterMembership(); + expression.AssignOwnership(returnParamMembership, resultFeature); + + Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature)); + + // Multiple ReturnParameterMemberships (illegal per OCL validation but tolerated) → first returned. + var secondResultFeature = new Feature(); + var secondReturnParamMembership = new ReturnParameterMembership(); + expression.AssignOwnership(secondReturnParamMembership, secondResultFeature); + + Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature)); } - + [Test] - public void ComputeResult_ThrowsNotSupportedException() + public void VerifyComputeModelLevelEvaluableOperation() { - Assert.That(() => ((IExpression)null).ComputeResult(), Throws.TypeOf()); + // Null guard on subject. + Assert.That(() => ((IExpression)null).ComputeModelLevelEvaluableOperation(new List()), Throws.TypeOf()); + + var expression = new Expression(); + + // visited == null must NOT throw (production coalesces null to empty list). + Assert.That(() => expression.ComputeModelLevelEvaluableOperation(null), Throws.Nothing); + + // Empty Expression (no specializations, no features) → true. + Assert.That(expression.ComputeModelLevelEvaluableOperation([]), Is.True); + + // One implied specialization, no features → true. + var impliedSpec = new Specialization { IsImplied = true, Specific = expression }; + expression.AssignOwnership(impliedSpec); + + Assert.That(expression.ComputeModelLevelEvaluableOperation([]), Is.True); + + // One non-implied specialization → false (early return). + var expressionNonImplied = new Expression(); + var nonImpliedSpec = new Specialization { IsImplied = false, Specific = expressionNonImplied }; + expressionNonImplied.AssignOwnership(nonImpliedSpec); + + Assert.That(expressionNonImplied.ComputeModelLevelEvaluableOperation([]), Is.False); + + // ownedFeature with Direction = In (BRANCH_A satisfied via in-parameter) → true. + var expressionInParam = new Expression(); + var inParamFeature = new Feature { Direction = FeatureDirectionKind.In }; + var inParamMembership = new FeatureMembership(); + expressionInParam.AssignOwnership(inParamMembership, inParamFeature); + + Assert.That(expressionInParam.ComputeModelLevelEvaluableOperation([]), Is.True); + + // ownedFeature equal to result (owned via ReturnParameterMembership) → BRANCH_A via f = result → true. + var expressionResult = new Expression(); + var resultFeature = new Feature(); + var returnParamMembership = new ReturnParameterMembership(); + expressionResult.AssignOwnership(returnParamMembership, resultFeature); + + Assert.That(expressionResult.ComputeModelLevelEvaluableOperation([]), Is.True); + + // ownedFeature with Direction = Out and not result, no nested features → BRANCH_A fails, BRANCH_B fails → false. + var expressionOutParam = new Expression(); + var outFeature = new Feature { Direction = FeatureDirectionKind.Out }; + var outMembership = new FeatureMembership(); + expressionOutParam.AssignOwnership(outMembership, outFeature); + + Assert.That(expressionOutParam.ComputeModelLevelEvaluableOperation([]), Is.False); + + // ownedFeature with a nested owned feature → BRANCH_A fails (hasNoNestedFeatures = false), + // BRANCH_B also fails (not ResultExpressionMembership) → false. + var expressionNestedFeature = new Expression(); + var featureWithNested = new Feature { Direction = FeatureDirectionKind.In }; + var nestedChild = new Feature(); + var nestedChildMembership = new FeatureMembership(); + featureWithNested.AssignOwnership(nestedChildMembership, nestedChild); + + var outerMembership = new FeatureMembership(); + expressionNestedFeature.AssignOwnership(outerMembership, featureWithNested); + + Assert.That(expressionNestedFeature.ComputeModelLevelEvaluableOperation([]), Is.False); + + // ownedFeature with a FeatureValue — NOTE: the production code's valuation check + // (f.valuation == null) is a known metamodel gap (IFeature has no Valuation property). + // FeatureValue is IOwningMembership but NOT IFeatureMembership, so the contained Expression + // is not visible via ownedFeature of featureWithValuation. BRANCH_A therefore succeeds + // (Direction = In, ownedFeature.Count == 0) and the operation returns true. + var expressionWithValuation = new Expression(); + var featureWithValuation = new Feature { Direction = FeatureDirectionKind.In }; + var featureValue = new FeatureValue(); + var featureValueExpr = new Expression(); + featureWithValuation.AssignOwnership(featureValue, featureValueExpr); + + var valuationMembership = new FeatureMembership(); + expressionWithValuation.AssignOwnership(valuationMembership, featureWithValuation); + + Assert.That(expressionWithValuation.ComputeModelLevelEvaluableOperation([]), Is.True); + + // BRANCH_B: ownedFeature under ResultExpressionMembership — STUB-BLOCKER. + // Although BRANCH_B logic only accesses owningFeatureMembership and calls ModelLevelEvaluable + // on the inner Expression, the production code first computes expressionSubject.result (line 157 + // of ExpressionExtensions.cs), which traverses featureMembership → inheritedMembership → + // ownedFeature → ResultExpressionMembership.ownedMemberFeature → ownedResultExpression → + // ResultExpressionMembershipExtensions.ComputeOwnedResultExpression, which is a stub. + // Therefore the BRANCH_B recursion path cannot be exercised until that stub is implemented. + var expressionBranchB = new Expression(); + var innerExpression = new Expression(); + var resultExprMembershipB = new ResultExpressionMembership(); + expressionBranchB.AssignOwnership(resultExprMembershipB, innerExpression); + + Assert.That( + () => expressionBranchB.ComputeModelLevelEvaluableOperation([]), + Throws.TypeOf()); + } + + [Test] + public void VerifyComputeEvaluateOperation() + { + // Null guard on subject. + Assert.That(() => ((IExpression)null).ComputeEvaluateOperation(null), Throws.TypeOf()); + + var expression = new Expression(); + + // target == null is permitted for the empty branch (base body doesn't dereference target). + Assert.That(() => expression.ComputeEvaluateOperation(null), Throws.Nothing); + + // Empty: no IResultExpressionMembership in ownedFeatureMembership → returns empty list. + Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty); + + // Discrimination: FeatureMembership with non-ResultExpressionMembership type → still empty. + var plainMembership = new FeatureMembership(); + var plainFeature = new Feature(); + expression.AssignOwnership(plainMembership, plainFeature); + + Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty); + + // STUB-BLOCKER: wire a ResultExpressionMembership → the production code calls + // resultExpressionMembership.ownedResultExpression, which dispatches to + // ResultExpressionMembershipExtensions.ComputeOwnedResultExpression, which is a stub + // that throws NotSupportedException. Assert the stub propagates. + var expressionWithRem = new Expression(); + var resultExprMembership = new ResultExpressionMembership(); + var innerExpression = new Expression(); + expressionWithRem.AssignOwnership(resultExprMembership, innerExpression); + + // ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub — NSE expected. + Assert.That( + () => expressionWithRem.ComputeEvaluateOperation(null), + Throws.TypeOf()); + } + + [Test] + public void VerifyComputeCheckConditionOperation() + { + // Null guard on subject. + Assert.That(() => ((IExpression)null).ComputeCheckConditionOperation(null), Throws.TypeOf()); + + var expression = new Expression(); + + // target == null: forwarded to Evaluate; empty-resultExprs branch returns [] → false (Count != 1). + Assert.That(expression.ComputeCheckConditionOperation(null), Is.False); + + // Empty: Evaluate returns [] (no ResultExpressionMembership) → false (Count != 1). + Assert.That(expression.ComputeCheckConditionOperation(null), Is.False); + + // STUB-BLOCKER: wire a ResultExpressionMembership → Evaluate delegates to ComputeEvaluateOperation, + // which accesses resultExpressionMembership.ownedResultExpression → + // ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub → NSE propagates. + var expressionWithRem = new Expression(); + var resultExprMembership = new ResultExpressionMembership(); + var innerExpression = new Expression(); + expressionWithRem.AssignOwnership(resultExprMembership, innerExpression); + + // ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub — NSE expected. + Assert.That( + () => expressionWithRem.ComputeCheckConditionOperation(null), + Throws.TypeOf()); } } } diff --git a/SysML2.NET/Extend/ExpressionExtensions.cs b/SysML2.NET/Extend/ExpressionExtensions.cs index 0acc31d1..a2289d5d 100644 --- a/SysML2.NET/Extend/ExpressionExtensions.cs +++ b/SysML2.NET/Extend/ExpressionExtensions.cs @@ -22,15 +22,12 @@ namespace SysML2.NET.Core.POCO.Kernel.Functions { 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.Kernel.Behaviors; - using SysML2.NET.Core.POCO.Root.Annotations; + using SysML2.NET.Core.POCO.Kernel.Expressions; using SysML2.NET.Core.POCO.Root.Elements; - using SysML2.NET.Core.POCO.Root.Namespaces; /// /// The class provides extensions methods for @@ -47,10 +44,11 @@ internal static class ExpressionExtensions /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IFunction ComputeFunction(this IExpression expressionSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + return expressionSubject == null + ? throw new ArgumentNullException(nameof(expressionSubject)) + : expressionSubject.type.OfType().FirstOrDefault(); } /// @@ -68,10 +66,9 @@ internal static IFunction ComputeFunction(this IExpression expressionSubject) /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static bool ComputeIsModelLevelEvaluable(this IExpression expressionSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + return expressionSubject?.ModelLevelEvaluable([]) ?? throw new ArgumentNullException(nameof(expressionSubject)); } /// @@ -96,10 +93,19 @@ internal static bool ComputeIsModelLevelEvaluable(this IExpression expressionSub /// /// the computed result /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static IFeature ComputeResult(this IExpression expressionSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (expressionSubject == null) + { + throw new ArgumentNullException(nameof(expressionSubject)); + } + + var resultParams = expressionSubject.featureMembership + .OfType() + .Select(returnParameterMembership => returnParameterMembership.ownedMemberParameter) + .ToList(); + + return resultParams.Count == 0 ? null : resultParams[0]; } /// @@ -132,10 +138,41 @@ internal static IFeature ComputeResult(this IExpression expressionSubject) /// /// The expected /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static bool ComputeModelLevelEvaluableOperation(this IExpression expressionSubject, List visited) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (expressionSubject == null) + { + throw new ArgumentNullException(nameof(expressionSubject)); + } + + visited ??= []; + + if (!expressionSubject.ownedSpecialization.All(specialization => specialization.IsImplied)) + { + return false; + } + + var resultFeature = expressionSubject.result; + + foreach (var ownedFeature in expressionSubject.ownedFeature) + { + // f.valuation == null clause omitted — IFeature has no Valuation property in the current POCO (metamodel gap). Follow-up issue required. + var branchA = + (expressionSubject.DirectionOf(ownedFeature) == FeatureDirectionKind.In + || ReferenceEquals(ownedFeature, resultFeature)) + && ownedFeature.ownedFeature.Count == 0; + + var branchB = + ownedFeature.owningFeatureMembership is IResultExpressionMembership + && (ownedFeature as IExpression)?.ModelLevelEvaluable(visited) == true; + + if (!(branchA || branchB)) + { + return false; + } + } + + return true; } /// @@ -169,10 +206,21 @@ internal static bool ComputeModelLevelEvaluableOperation(this IExpression expres /// /// The expected collection of /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static List ComputeEvaluateOperation(this IExpression expressionSubject, IElement target) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (expressionSubject == null) + { + throw new ArgumentNullException(nameof(expressionSubject)); + } + + var resultExprs = expressionSubject.ownedFeatureMembership + .OfType() + .Select(resultExpressionMembership => resultExpressionMembership.ownedResultExpression) + .ToList(); + + return resultExprs.Count == 0 + ? [] + : resultExprs[0].Evaluate(target); } /// @@ -197,10 +245,16 @@ internal static List ComputeEvaluateOperation(this IExpression express /// /// The expected /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static bool ComputeCheckConditionOperation(this IExpression expressionSubject, IElement target) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + if (expressionSubject == null) + { + throw new ArgumentNullException(nameof(expressionSubject)); + } + + var results = expressionSubject.Evaluate(target); + + return results.Count == 1 && results[0] is ILiteralBoolean { Value: true }; } } }