diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 8690d0498..d2cfd32a2 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -55,6 +55,7 @@ This is currently supported on `Features` (e.g. `Attribute`), `Constraints` and It leverages the selection dialog to either create an _occurrence timeslice/snapshot_, or the _usage timeslice/snapshot_ matching the `OccurrenceDefinition` on which the tool is applied. - https://github.com/eclipse-syson/syson/issues/2250[#2250] [diagrams] Leverage the selection dialog to improve the graphical node tools creating a _require_ `ConstraintUsage`, or an _assume_ `ConstraintUsage`, from `RequirementUsage` and `RequirementDefinition` graphical nodes. - https://github.com/eclipse-syson/syson/issues/2254[#2254] [diagrams] Add the support for _assume_ and _require_ graphical edges. +- https://github.com/eclipse-syson/syson/issues/2260[#2260] [diagrams] Add the _New Assume Constraint_ or _New Require Constraint_ edge tools to create _assume_ and _require_ graphical edges. == v2026.5.0 diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVRequirementConstraintMembershipTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVRequirementConstraintMembershipTests.java new file mode 100644 index 000000000..c383e0eeb --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVRequirementConstraintMembershipTests.java @@ -0,0 +1,203 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson.application.controllers.diagrams.general.view; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; +import static org.eclipse.sirius.components.diagrams.tests.DiagramEventPayloadConsumer.assertRefreshedDiagramThat; +import static org.eclipse.sirius.components.diagrams.tests.assertions.DiagramInstanceOfAssertFactories.EDGE; +import static org.eclipse.sirius.components.diagrams.tests.assertions.DiagramInstanceOfAssertFactories.EDGE_STYLE; + +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramEventInput; +import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramRefreshedEventPayload; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.components.diagrams.ArrowStyle; +import org.eclipse.sirius.components.diagrams.Diagram; +import org.eclipse.sirius.components.diagrams.Edge; +import org.eclipse.sirius.components.diagrams.Label; +import org.eclipse.sirius.components.diagrams.ViewModifier; +import org.eclipse.sirius.components.view.emf.diagram.IDiagramIdProvider; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.eclipse.syson.AbstractIntegrationTests; +import org.eclipse.syson.GivenSysONServer; +import org.eclipse.syson.application.controller.editingcontext.checkers.SemanticCheckerService; +import org.eclipse.syson.application.controllers.diagrams.testers.EdgeCreationTester; +import org.eclipse.syson.application.data.GeneralViewWithTopNodesTestProjectData; +import org.eclipse.syson.services.SemanticRunnableFactory; +import org.eclipse.syson.services.diagrams.DiagramComparator; +import org.eclipse.syson.services.diagrams.DiagramDescriptionIdProvider; +import org.eclipse.syson.services.diagrams.api.IGivenDiagramDescription; +import org.eclipse.syson.services.diagrams.api.IGivenDiagramSubscription; +import org.eclipse.syson.standard.diagrams.view.SDVDescriptionNameGenerator; +import org.eclipse.syson.sysml.RequirementConstraintMembership; +import org.eclipse.syson.sysml.SysmlPackage; +import org.eclipse.syson.sysml.metamodel.helper.LabelConstants; +import org.eclipse.syson.util.IDescriptionNameGenerator; +import org.eclipse.syson.util.SysONRepresentationDescriptionIdentifiers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +/** + * {@link org.eclipse.syson.sysml.RequirementConstraintMembership} related test in General View. + * + * @author gcoutable + */ +@Transactional +@GivenSysONServer({ GeneralViewWithTopNodesTestProjectData.SCRIPT_PATH }) +@SuppressWarnings("checkstyle:MultipleStringLiterals") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class GVRequirementConstraintMembershipTests extends AbstractIntegrationTests { + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private IGivenDiagramSubscription givenDiagramSubscription; + + @Autowired + private IGivenDiagramDescription givenDiagramDescription; + + @Autowired + private IDiagramIdProvider diagramIdProvider; + + @Autowired + private DiagramComparator diagramComparator; + + @Autowired + private SemanticRunnableFactory semanticRunnableFactory; + + @Autowired + private IObjectSearchService objectSearchService; + + @Autowired + private EdgeCreationTester edgeCreationTester; + + private final IDescriptionNameGenerator descriptionNameGenerator = new SDVDescriptionNameGenerator(); + + private SemanticCheckerService semanticCheckerService; + + private Flux givenSubscriptionToDiagram() { + var diagramEventInput = new DiagramEventInput(UUID.randomUUID(), GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, GeneralViewWithTopNodesTestProjectData.GraphicalIds.DIAGRAM_ID); + return this.givenDiagramSubscription.subscribe(diagramEventInput); + } + + @BeforeEach + public void setUp() { + this.givenInitialServerState.initialize(); + this.semanticCheckerService = new SemanticCheckerService(this.semanticRunnableFactory, this.objectSearchService, GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, + GeneralViewWithTopNodesTestProjectData.SemanticIds.PACKAGE_1_ID); + } + + @DisplayName("GIVEN the general view diagram, WHEN the assume edge tool between a RequirementUsage graphical node and a ConstraintUsage graphical node is used, THEN it creates an assume edge between both elements") + @Test + public void testCreateAssumeConstraintWithEdgeToolBetweenRequirementUsageAndConstraintUsage() { + this.createRequirementConstraintMembershipWithEdgeTool(SysmlPackage.eINSTANCE.getRequirementUsage(), "New Assume Constraint", GeneralViewWithTopNodesTestProjectData.GraphicalIds.REQUIREMENT_USAGE_ID, LabelConstants.OPEN_QUOTE + LabelConstants.ASSUME + LabelConstants.CLOSE_QUOTE, "requirement"); + } + + @DisplayName("GIVEN the general view diagram, WHEN the assume edge tool between a RequirementDefinition graphical node and a ConstraintUsage graphical node is used, THEN it creates an assume edge between both elements") + @Test + public void testCreateAssumeConstraintWithEdgeToolBetweenRequirementDefinitionAndConstraintUsage() { + this.createRequirementConstraintMembershipWithEdgeTool(SysmlPackage.eINSTANCE.getRequirementDefinition(), "New Assume Constraint", GeneralViewWithTopNodesTestProjectData.GraphicalIds.REQUIREMENT_DEFINITION_ID, LabelConstants.OPEN_QUOTE + LabelConstants.ASSUME + LabelConstants.CLOSE_QUOTE, "RequirementDefinition"); + } + + @DisplayName("GIVEN the general view diagram, WHEN the require edge tool between a RequirementUsage graphical node and a ConstraintUsage graphical node is used, THEN it creates a require edge between both elements") + @Test + public void testCreateRequireConstraintWithEdgeToolBetweenRequirementUsageAndConstraintUsage() { + this.createRequirementConstraintMembershipWithEdgeTool(SysmlPackage.eINSTANCE.getRequirementUsage(), "New Require Constraint", GeneralViewWithTopNodesTestProjectData.GraphicalIds.REQUIREMENT_USAGE_ID, LabelConstants.OPEN_QUOTE + LabelConstants.REQUIRE + LabelConstants.CLOSE_QUOTE, "requirement"); + } + + @DisplayName("GIVEN the general view diagram, WHEN the require edge tool between a RequirementDefinition graphical node and a ConstraintUsage graphical node is used, THEN it creates a require edge between both elements") + @Test + public void testCreateRequireConstraintWithEdgeToolBetweenRequirementDefinitionAndConstraintUsage() { + this.createRequirementConstraintMembershipWithEdgeTool(SysmlPackage.eINSTANCE.getRequirementDefinition(), "New Require Constraint", GeneralViewWithTopNodesTestProjectData.GraphicalIds.REQUIREMENT_DEFINITION_ID, LabelConstants.OPEN_QUOTE + LabelConstants.REQUIRE + LabelConstants.CLOSE_QUOTE, "RequirementDefinition"); + } + + private void createRequirementConstraintMembershipWithEdgeTool(EClass parentEClass, String toolName, String graphicalSourceNodeId, String expectedEdgeLabel, String parentLabel) { + var flux = this.givenSubscriptionToDiagram(); + + var diagramDescription = this.givenDiagramDescription.getDiagramDescription(GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, + SysONRepresentationDescriptionIdentifiers.GENERAL_VIEW_DIAGRAM_DESCRIPTION_ID); + var diagramDescriptionIdProvider = new DiagramDescriptionIdProvider(diagramDescription, this.diagramIdProvider); + + var edgeCreationToolId = diagramDescriptionIdProvider.getEdgeCreationToolId( + this.descriptionNameGenerator.getNodeName(parentEClass), + toolName); + + AtomicReference diagram = new AtomicReference<>(); + Consumer initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set); + + Runnable createEdgeRunnable = () -> this.edgeCreationTester.createEdgeUsingNodeId(GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, + diagram, + graphicalSourceNodeId, + GeneralViewWithTopNodesTestProjectData.GraphicalIds.CONSTRAINT_USAGE_ID, + edgeCreationToolId); + + Consumer diagramContentConsumerAfterNewEdge = assertRefreshedDiagramThat(newDiagram -> { + var newEdges = this.diagramComparator.newEdges(diagram.get(), newDiagram); + var newVisibleEdges = newEdges.stream().filter(edge -> edge.getState() != ViewModifier.Hidden).toList(); + + assertThat(newVisibleEdges).hasSize(1).first(EDGE) + .hasSourceId(graphicalSourceNodeId) + .hasTargetId(GeneralViewWithTopNodesTestProjectData.GraphicalIds.CONSTRAINT_USAGE_ID) + .extracting(Edge::getCenterLabel) + .extracting(Label::text) + .hasToString(expectedEdgeLabel); + + assertThat(newVisibleEdges).hasSize(1).first(EDGE) + .extracting(Edge::getStyle, EDGE_STYLE) + .hasSourceArrow(ArrowStyle.None) + .hasTargetArrow(ArrowStyle.InputArrow); + }); + + Consumer additionalCheck = object -> { + assertThat(object).isInstanceOf(List.class) + .asInstanceOf(type(List.class)) + .satisfies(requirementConstraintMemberships -> { + assertThat((List) requirementConstraintMemberships).size().isEqualTo(1); + assertThat(requirementConstraintMemberships.getFirst()) + .isInstanceOf(RequirementConstraintMembership.class) + .asInstanceOf(type(RequirementConstraintMembership.class)) + .satisfies(requirementConstraintMembership -> { + var ownedConstraint = requirementConstraintMembership.getOwnedConstraint(); + var ownedSubsetting = ownedConstraint.getOwnedSubsetting(); + assertThat(ownedSubsetting).isNotEmpty(); + assertThat(ownedSubsetting.getFirst().getSubsettedFeature()).isEqualTo(requirementConstraintMembership.getReferencedConstraint()); + }); + }); + }; + + Runnable semanticCheck = this.semanticCheckerService.checkEditingContext(this.semanticCheckerService.getElementInParentSemanticChecker(parentLabel, SysmlPackage.eINSTANCE.getElement_OwnedRelationship(), SysmlPackage.eINSTANCE.getRequirementConstraintMembership(), additionalCheck)); + + StepVerifier.create(flux) + .consumeNextWith(initialDiagramContentConsumer) + .then(createEdgeRunnable) + .consumeNextWith(diagramContentConsumerAfterNewEdge) + .then(semanticCheck) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } +} diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewWithTopNodesTestProjectData.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewWithTopNodesTestProjectData.java index efebbc237..b0a24bc44 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewWithTopNodesTestProjectData.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewWithTopNodesTestProjectData.java @@ -38,6 +38,8 @@ public static class GraphicalIds { public static final String CONCERN_USAGE_ID = "0999b8c3-d37c-3644-a1d6-b9777a499d11"; + public static final String CONSTRAINT_USAGE_ID = "22da3b61-32f6-346e-9deb-7dd1f64bfd69"; + public static final String DIAGRAM_ID = "fa8c8a8d-2813-404c-876f-c04e8b297134"; public static final String ITEM_DEFINITION_ID = "df3542d9-6314-3da5-993c-a296f4042674"; diff --git a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolService.java b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolService.java index 23872290c..c8ccc8ad1 100644 --- a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolService.java +++ b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolService.java @@ -31,6 +31,7 @@ import org.eclipse.syson.diagram.services.aql.DiagramMutationAQLService; import org.eclipse.syson.diagram.services.aql.DiagramQueryAQLService; import org.eclipse.syson.model.services.aql.ModelMutationAQLService; +import org.eclipse.syson.sysml.RequirementConstraintKind; import org.eclipse.syson.sysml.SysmlPackage; import org.eclipse.syson.sysml.Usage; import org.eclipse.syson.util.AQLConstants; @@ -682,4 +683,56 @@ public EdgeTool createFramedConcernEdgeTool() { .targetElementDescriptions(targetNodeDescriptions.toArray(NodeDescription[]::new)) .build(); } + + public EdgeTool createAssumeConstraintEdgeTool() { + var builder = this.diagramBuilderHelper.newEdgeTool(); + var body = this.viewBuilderHelper.newChangeContext() + .expression(ServiceMethod.of2(ModelMutationAQLService::createConstraint).aql(EdgeDescription.SEMANTIC_EDGE_SOURCE, EdgeDescription.SEMANTIC_EDGE_TARGET, "assumptionConstraint")); + + var targetNodeDescriptions = new ArrayList(); + var constraintUsageNodeDescriptionName = this.nameGenerator.getNodeName(SysmlPackage.eINSTANCE.getConstraintUsage()); + this.allNodeDescriptions.stream() + .filter(nd -> constraintUsageNodeDescriptionName.equals(nd.getName())) + .findFirst() + .ifPresent(targetNodeDescriptions::add); + + String requirementConstraintKindType = SysMLMetamodelHelper.buildQualifiedName(SysmlPackage.eINSTANCE.getRequirementConstraintKind()); + var letRequirementConstraintLiteral = this.viewBuilderHelper.newLet() + .variableName("assumptionConstraint") + .valueExpression(AQLConstants.AQL + requirementConstraintKindType + ".getEEnumLiteral('" + RequirementConstraintKind.ASSUMPTION.getLiteral() + "').instance") + .children(body.build()); + + return builder + .name(this.nameGenerator.getCreationToolName("New Assume ", SysmlPackage.eINSTANCE.getConstraintUsage())) + .iconURLsExpression(METAMODEL_ICONS_PATH + SysmlPackage.eINSTANCE.getRequirementConstraintMembership().getName() + SVG) + .body(letRequirementConstraintLiteral.build()) + .targetElementDescriptions(targetNodeDescriptions.toArray(NodeDescription[]::new)) + .build(); + } + + public EdgeTool createRequireConstraintEdgeTool() { + var builder = this.diagramBuilderHelper.newEdgeTool(); + var body = this.viewBuilderHelper.newChangeContext() + .expression(ServiceMethod.of2(ModelMutationAQLService::createConstraint).aql(EdgeDescription.SEMANTIC_EDGE_SOURCE, EdgeDescription.SEMANTIC_EDGE_TARGET, "requirementConstraint")); + + var targetNodeDescriptions = new ArrayList(); + var constraintUsageNodeDescriptionName = this.nameGenerator.getNodeName(SysmlPackage.eINSTANCE.getConstraintUsage()); + this.allNodeDescriptions.stream() + .filter(nd -> constraintUsageNodeDescriptionName.equals(nd.getName())) + .findFirst() + .ifPresent(targetNodeDescriptions::add); + + String requirementConstraintKindType = SysMLMetamodelHelper.buildQualifiedName(SysmlPackage.eINSTANCE.getRequirementConstraintKind()); + var letRequirementConstraintLiteral = this.viewBuilderHelper.newLet() + .variableName("requirementConstraint") + .valueExpression(AQLConstants.AQL + requirementConstraintKindType + ".getEEnumLiteral('" + RequirementConstraintKind.REQUIREMENT.getLiteral() + "').instance") + .children(body.build()); + + return builder + .name(this.nameGenerator.getCreationToolName("New Require ", SysmlPackage.eINSTANCE.getConstraintUsage())) + .iconURLsExpression(METAMODEL_ICONS_PATH + SysmlPackage.eINSTANCE.getRequirementConstraintMembership().getName() + SVG) + .body(letRequirementConstraintLiteral.build()) + .targetElementDescriptions(targetNodeDescriptions.toArray(NodeDescription[]::new)) + .build(); + } } diff --git a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolSwitch.java b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolSwitch.java index 2cd07e9cf..aeedf97be 100644 --- a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolSwitch.java +++ b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolSwitch.java @@ -258,6 +258,8 @@ public List casePortUsage(PortUsage object) { public List caseRequirementDefinition(RequirementDefinition object) { var edgeTools = new ArrayList(); edgeTools.add(this.edgeToolService.createFramedConcernEdgeTool()); + edgeTools.add(this.edgeToolService.createAssumeConstraintEdgeTool()); + edgeTools.add(this.edgeToolService.createRequireConstraintEdgeTool()); edgeTools.addAll(this.caseDefinition(object)); return edgeTools; } @@ -277,6 +279,8 @@ public List caseRequirementUsage(RequirementUsage object) { .toList(); edgeTools.add(this.edgeToolService.createBecomeObjectiveRequirementEdgeTool(objectiveTargets)); edgeTools.add(this.edgeToolService.createFramedConcernEdgeTool()); + edgeTools.add(this.edgeToolService.createAssumeConstraintEdgeTool()); + edgeTools.add(this.edgeToolService.createRequireConstraintEdgeTool()); edgeTools.addAll(this.caseUsage(object)); return edgeTools; } diff --git a/doc/content/modules/user-manual/assets/images/release-notes-assume-and-require-edge-tools.png b/doc/content/modules/user-manual/assets/images/release-notes-assume-and-require-edge-tools.png new file mode 100644 index 000000000..e21a91a54 Binary files /dev/null and b/doc/content/modules/user-manual/assets/images/release-notes-assume-and-require-edge-tools.png differ diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.7.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.7.0.adoc index 10636ca24..34fdeac1a 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.7.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.7.0.adoc @@ -34,10 +34,15 @@ image::release-notes-frames-compartment.png[frames compartment displaying concer It leverages the selection dialog to either create an _occurrence timeslice/snapshot_, or the _usage timeslice/snapshot_ matching the `OccurrenceDefinition` on which the tool is applied. ** Add the support for _assume_ and _require_ graphical edges. -These edges are displayed using the tool to create a _require_ `ConstraintUsage`, or an _assume_ `ConstraintUsage`, from `RequirementUsage` and `RequirementDefinition` graphical nodes, when another `ConstraintUsage` is selected. +These graphical edges are displayed using the tool to create a _require_ `ConstraintUsage`, or an _assume_ `ConstraintUsage`, from `RequirementUsage` and `RequirementDefinition` graphical nodes, when another `ConstraintUsage` is selected. +These graphical edges can also be created using the _New Assume Constraint_ or _New Require Constraint_ graphical edge tools. + image::release-notes-assume-and-require-edges.png[assume and require edge between a RequirementDefinition and two different ConstraintUsage, width=60%,height=60%] +** Add the _New Assume Constraint_ or _New Require Constraint_ edge tools to create _assume_ and _require_ graphical edges. ++ +image::release-notes-assume-and-require-edge-tools.png[list available edge tools containing the two new tools, between a RequirementUsage and a ConstraintUsage, width=60%,height=60%] + * In the _Explorer_ view: ** The tree items corresponding to the internals of `Expression` elements (syntax tree) are now hidden by default.