diff --git a/backend/metamodel/syson-siriusweb-customnodes-metamodel/src/test/java/org/eclipse/syson/customnodes/metamodel/SysMLCustomnodesSwitchTest.java b/backend/metamodel/syson-siriusweb-customnodes-metamodel/src/test/java/org/eclipse/syson/customnodes/metamodel/SysMLCustomnodesSwitchTest.java new file mode 100644 index 0000000000..2e4660c4e2 --- /dev/null +++ b/backend/metamodel/syson-siriusweb-customnodes-metamodel/src/test/java/org/eclipse/syson/customnodes/metamodel/SysMLCustomnodesSwitchTest.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * 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.customnodes.metamodel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.eclipse.emf.ecore.EcoreFactory; +import org.eclipse.sirius.components.view.FixedColor; +import org.eclipse.sirius.components.view.ViewFactory; +import org.eclipse.syson.sysmlcustomnodes.SysMLCustomnodesFactory; +import org.eclipse.syson.sysmlcustomnodes.SysMLCustomnodesPackage; +import org.eclipse.syson.sysmlcustomnodes.SysMLImportedPackageNodeStyleDescription; +import org.eclipse.syson.sysmlcustomnodes.SysMLNoteNodeStyleDescription; +import org.eclipse.syson.sysmlcustomnodes.SysMLPackageNodeStyleDescription; +import org.eclipse.syson.sysmlcustomnodes.SysMLViewFrameNodeStyleDescription; +import org.eclipse.syson.sysmlcustomnodes.util.SysMLCustomnodesAdapterFactory; +import org.eclipse.syson.sysmlcustomnodes.util.SysMLCustomnodesSwitch; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the generated custom-node switch and adapter factory. + *
+ * The tests verify that each concrete custom-node style is dispatched to its dedicated switch case and accepted by + * the model adapter factory. This protects the EMF classifier-to-type wiring used when Sirius Web reads custom node + * style descriptions, while providing high-value coverage without constructing a representation or starting Spring. + *
+ *+ * They do not verify adapter implementations because the generated factory intentionally returns no adapters. They + * also do not validate the visual rendering of the styles, which is the responsibility of the diagram runtime. + *
+ * + * @author arichard + */ +class SysMLCustomnodesSwitchTest { + + /** + * Verifies dispatch to each custom-node style case. + */ + @DisplayName("GIVEN each custom-node style, WHEN switching on it, THEN its dedicated case is selected") + @Test + void testSwitchDispatchesConcreteCustomNodeStyles() { + SysMLCustomnodesSwitch+ * These tests exercise the service with SysML model objects and mocked Sirius Web lookup services. They protect the + * expected availability of control-node tools and the interpretation of subject, state-subaction, receiver, payload, + * and inherited-member compartments without requiring a running Sirius Web application. + *
+ *+ * They do not validate the AQL expressions that invoke this service, Spring dependency injection, or the rendering of + * the resulting diagram tools and compartments. Those concerns belong to view-definition and integration tests. + *
+ * + * @author arichard + */ +class DiagramQueryToolServiceTest { + + @DisplayName("GIVEN selected nodes, WHEN checking a control-node action, THEN only action usages and definitions are accepted") + @Test + void testIsControlNodeActionCreationToolInAction() { + IObjectSearchService objectSearchService = mock(IObjectSearchService.class); + IEditingContext editingContext = mock(IEditingContext.class); + Node selectedNode = mock(Node.class); + when(selectedNode.getTargetObjectId()).thenReturn("selected-node"); + DiagramQueryToolService service = this.createService(objectSearchService); + + when(objectSearchService.getObject(editingContext, "selected-node")).thenReturn(Optional.of(SysmlFactory.eINSTANCE.createActionUsage())); + assertThat(service.isControlNodeActionCreationToolInAction(editingContext, selectedNode)).isTrue(); + + when(objectSearchService.getObject(editingContext, "selected-node")).thenReturn(Optional.of(SysmlFactory.eINSTANCE.createActionDefinition())); + assertThat(service.isControlNodeActionCreationToolInAction(editingContext, selectedNode)).isTrue(); + + when(objectSearchService.getObject(editingContext, "selected-node")).thenReturn(Optional.of(SysmlFactory.eINSTANCE.createPartUsage())); + assertThat(service.isControlNodeActionCreationToolInAction(editingContext, selectedNode)).isFalse(); + + when(objectSearchService.getObject(editingContext, "selected-node")).thenReturn(Optional.empty()); + assertThat(service.isControlNodeActionCreationToolInAction(editingContext, selectedNode)).isFalse(); + } + + @DisplayName("GIVEN a requirement, WHEN checking its subject compartment, THEN its subject membership determines whether it is empty") + @Test + void testIsEmptySubjectCompartment() { + DiagramQueryToolService service = this.createService(mock(IObjectSearchService.class)); + RequirementUsage requirementUsage = SysmlFactory.eINSTANCE.createRequirementUsage(); + + assertThat(service.isEmptySubjectCompartment(requirementUsage)).isTrue(); + + requirementUsage.getOwnedRelationship().add(SysmlFactory.eINSTANCE.createSubjectMembership()); + assertThat(service.isEmptySubjectCompartment(requirementUsage)).isFalse(); + assertThat(service.isEmptySubjectCompartment(SysmlFactory.eINSTANCE.createPartUsage())).isTrue(); + } + + @DisplayName("GIVEN a state, WHEN checking a subaction compartment, THEN a matching action kind makes it non-empty") + @Test + void testIsEmptyOfActionKindCompartment() { + DiagramQueryToolService service = this.createService(mock(IObjectSearchService.class)); + StateUsage stateUsage = SysmlFactory.eINSTANCE.createStateUsage(); + + assertThat(service.isEmptyOfActionKindCompartment(stateUsage, "entry")).isTrue(); + + StateSubactionMembership membership = SysmlFactory.eINSTANCE.createStateSubactionMembership(); + membership.setKind(org.eclipse.syson.sysml.StateSubactionKind.ENTRY); + stateUsage.getOwnedRelationship().add(membership); + assertThat(service.isEmptyOfActionKindCompartment(stateUsage, "ENTRY")).isFalse(); + assertThat(service.isEmptyOfActionKindCompartment(SysmlFactory.eINSTANCE.createPartUsage(), "entry")).isTrue(); + } + + @DisplayName("GIVEN a use case, WHEN checking its objective compartment, THEN its objective membership determines whether it is empty") + @Test + void testIsEmptyObjectiveRequirementCompartment() { + DiagramQueryToolService service = this.createService(mock(IObjectSearchService.class)); + UseCaseUsage useCaseUsage = SysmlFactory.eINSTANCE.createUseCaseUsage(); + + assertThat(service.isEmptyObjectiveRequirementCompartment(useCaseUsage)).isTrue(); + + useCaseUsage.getOwnedRelationship().add(SysmlFactory.eINSTANCE.createObjectiveMembership()); + assertThat(service.isEmptyObjectiveRequirementCompartment(useCaseUsage)).isFalse(); + } + + @DisplayName("GIVEN an accept action, WHEN checking receiver and payload compartments, THEN memberships and typings determine whether they are empty") + @Test + void testIsEmptyAcceptActionUsageCompartments() { + DiagramQueryToolService service = this.createService(mock(IObjectSearchService.class)); + AcceptActionUsage acceptActionUsage = mock(AcceptActionUsage.class); + + assertThat(service.isEmptyAcceptActionUsageReceiver(SysmlFactory.eINSTANCE.createAcceptActionUsage())).isTrue(); + assertThat(service.isEmptyAcceptActionUsagePayload(SysmlFactory.eINSTANCE.createPartUsage())).isTrue(); + + Expression receiver = mock(Expression.class); + Membership receiverMembership = SysmlFactory.eINSTANCE.createMembership(); + when(acceptActionUsage.getReceiverArgument()).thenReturn(receiver); + when(receiver.getOwnedRelationship()).thenReturn(new BasicEList+ * The switch identifies semantic relationships that must be deleted together when one of their referenced elements + * is removed. These tests cover the supported relationship kinds, their matching structural features, and irrelevant + * features that must not produce additional deletions. They provide focused regression coverage for deletion planning + * using only in-memory SysML objects. + *
+ *+ * They do not perform a deletion, persist a model, or verify the Explorer and diagram user interfaces. Those effects + * are performed by higher-level services and are outside the scope of this switch unit test. + *
+ * + * @author arichard + */ +class RelatedElementsSwitchTest { + + @DisplayName("GIVEN relationships, WHEN their referenced feature is deleted, THEN only the corresponding relationships are returned") + @Test + void testSimpleRelationshipCases() { + Annotation annotation = SysmlFactory.eINSTANCE.createAnnotation(); + FeatureTyping featureTyping = SysmlFactory.eINSTANCE.createFeatureTyping(); + Redefinition redefinition = SysmlFactory.eINSTANCE.createRedefinition(); + ReferenceSubsetting referenceSubsetting = SysmlFactory.eINSTANCE.createReferenceSubsetting(); + Subclassification subclassification = SysmlFactory.eINSTANCE.createSubclassification(); + Subsetting subsetting = SysmlFactory.eINSTANCE.createSubsetting(); + + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getRelationship_OwnedRelatedElement()).caseAnnotation(annotation)).containsExactly(annotation); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getFeatureTyping_Type()).caseFeatureTyping(featureTyping)).containsExactly(featureTyping); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getRedefinition_RedefinedFeature()).caseRedefinition(redefinition)).containsExactly(redefinition); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getReferenceSubsetting_ReferencedFeature()).caseReferenceSubsetting(referenceSubsetting)) + .containsExactly(referenceSubsetting); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getSubclassification_Superclassifier()).caseSubclassification(subclassification)) + .containsExactly(subclassification); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getSubsetting_SubsettedFeature()).caseSubsetting(subsetting)).containsExactly(subsetting); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getFeatureTyping_Type()).caseAnnotation(annotation)).isEmpty(); + } + + @DisplayName("GIVEN a dependency inside a membership, WHEN a client or supplier is deleted, THEN both objects are returned") + @Test + void testDependencyCases() { + Dependency dependency = SysmlFactory.eINSTANCE.createDependency(); + Membership membership = SysmlFactory.eINSTANCE.createMembership(); + membership.getOwnedRelatedElement().add(dependency); + + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getDependency_Client()).caseDependency(dependency)).containsExactlyInAnyOrder(dependency, membership); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getDependency_Supplier()).caseDependency(dependency)).containsExactlyInAnyOrder(dependency, membership); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getFeatureTyping_Type()).caseDependency(dependency)).isEmpty(); + } + + @DisplayName("GIVEN a succession, WHEN either end is deleted, THEN the succession is returned unless an end was already deleted") + @Test + void testSuccessionCases() { + Succession succession = SysmlFactory.eINSTANCE.createSuccession(); + + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getRelationship_Source()).caseSuccession(succession)).containsExactly(succession); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getRelationship_Target()).caseSuccession(succession)).containsExactly(succession); + assertThat(new RelatedElementsSwitch(SysmlPackage.eINSTANCE.getFeatureTyping_Type()).caseSuccession(succession)).isEmpty(); + } +} diff --git a/backend/services/syson-services/src/test/java/org/eclipse/syson/services/ToolServiceTest.java b/backend/services/syson-services/src/test/java/org/eclipse/syson/services/ToolServiceTest.java new file mode 100644 index 0000000000..0537b536ad --- /dev/null +++ b/backend/services/syson-services/src/test/java/org/eclipse/syson/services/ToolServiceTest.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * 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.services; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.sirius.components.collaborative.diagrams.DiagramContext; +import org.eclipse.sirius.components.core.api.IFeedbackMessageService; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.components.diagrams.Diagram; +import org.eclipse.sirius.components.diagrams.Edge; +import org.eclipse.sirius.components.diagrams.INodeStyle; +import org.eclipse.sirius.components.diagrams.ListLayoutStrategy; +import org.eclipse.sirius.components.diagrams.Node; +import org.eclipse.syson.services.api.ISysMLMoveElementService; +import org.eclipse.syson.sysml.Element; +import org.eclipse.syson.sysml.SysmlFactory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the diagram-tree traversal helpers provided by {@link ToolService}. + *+ * The tests construct small mocked diagram hierarchies to verify parent and source-node lookup, child-node + * collection, and semantic-node identity matching. They provide fast regression coverage for the structural logic + * used by diagram tools, including nested and border nodes, without depending on a persisted representation. + *
+ *+ * They do not validate diagram creation, event handling, or a complete Sirius Web tool execution. Those behaviours + * require the surrounding application services and are intentionally outside this unit-test suite. + *
+ * + * @author arichard + */ +class ToolServiceTest { + + @DisplayName("GIVEN a diagram hierarchy, WHEN finding a parent, THEN direct and nested diagram elements resolve to their container") + @Test + void testGetParentNode() { + ToolService service = this.createService(mock(IIdentityService.class)); + Diagram diagram = mock(Diagram.class); + DiagramContext diagramContext = mock(DiagramContext.class); + when(diagramContext.diagram()).thenReturn(diagram); + Node topLevelNode = this.node("top-level"); + Node containerNode = this.node("container"); + Node nestedNode = this.node("nested"); + Edge edge = mock(Edge.class); + when(containerNode.getChildNodes()).thenReturn(new ArrayList<>(List.of(nestedNode))); + when(diagram.getNodes()).thenReturn(new ArrayList<>(List.of(topLevelNode, containerNode))); + when(diagram.getEdges()).thenReturn(new ArrayList<>(List.of(edge))); + Element element = SysmlFactory.eINSTANCE.createPartUsage(); + + assertThat(service.getParentNode(element, topLevelNode, diagramContext)).isSameAs(diagram); + assertThat(service.getParentNode(element, edge, diagramContext)).isSameAs(diagram); + assertThat(service.getParentNode(element, nestedNode, diagramContext)).isSameAs(containerNode); + assertThat(service.getParentNode(element, mock(Node.class), diagramContext)).isNull(); + } + + @DisplayName("GIVEN nodes and borders, WHEN looking up an edge source, THEN nested and border nodes are found") + @Test + void testGetSourceNode() { + ToolService service = this.createService(mock(IIdentityService.class)); + Diagram diagram = mock(Diagram.class); + Node root = this.node("root"); + Node child = this.node("child"); + Node border = this.node("border"); + when(root.getChildNodes()).thenReturn(new ArrayList<>(List.of(child))); + when(root.getBorderNodes()).thenReturn(new ArrayList<>(List.of(border))); + when(diagram.getNodes()).thenReturn(new ArrayList<>(List.of(root))); + Edge edge = mock(Edge.class); + + when(edge.getSourceId()).thenReturn("root"); + assertThat(service.getSourceNode(edge, diagram)).isSameAs(root); + when(edge.getSourceId()).thenReturn("child"); + assertThat(service.getSourceNode(edge, diagram)).isSameAs(child); + when(edge.getSourceId()).thenReturn("border"); + assertThat(service.getSourceNode(edge, diagram)).isSameAs(border); + when(edge.getSourceId()).thenReturn("missing"); + assertThat(service.getSourceNode(edge, diagram)).isNull(); + } + + @DisplayName("GIVEN a selected node or a diagram, WHEN collecting children, THEN list compartments and top-level nodes are included") + @Test + void testGetChildNodesAndIsPresent() { + IIdentityService identityService = mock(IIdentityService.class); + ToolService service = this.createService(identityService); + Diagram diagram = mock(Diagram.class); + DiagramContext diagramContext = mock(DiagramContext.class); + when(diagramContext.diagram()).thenReturn(diagram); + Node listNode = this.node("list"); + Node compartment = this.node("compartment"); + Node item = this.node("item"); + INodeStyle style = mock(INodeStyle.class); + when(style.getChildrenLayoutStrategy()).thenReturn(ListLayoutStrategy.newListLayoutStrategy().build()); + when(listNode.getStyle()).thenReturn(style); + when(listNode.getChildNodes()).thenReturn(new ArrayList<>(List.of(compartment))); + when(compartment.getChildNodes()).thenReturn(new ArrayList<>(List.of(item))); + Node topLevelNode = this.node("top-level"); + when(diagram.getNodes()).thenReturn(new ArrayList<>(List.of(topLevelNode))); + + assertThat(service.getChildNodes(diagramContext, listNode)).containsExactly(compartment, item); + assertThat(service.getChildNodes(diagramContext, null)).containsExactly(topLevelNode); + + Element element = SysmlFactory.eINSTANCE.createPartUsage(); + when(identityService.getId(element)).thenReturn("item"); + assertThat(service.isPresent(element, List.of(compartment, item))).isTrue(); + when(identityService.getId(element)).thenReturn("missing"); + assertThat(service.isPresent(element, List.of(compartment, item))).isFalse(); + } + + private ToolService createService(IIdentityService identityService) { + return new ToolService(identityService, mock(IObjectSearchService.class), mock(IFeedbackMessageService.class), mock(ISysMLMoveElementService.class)); + } + + private Node node(String id) { + Node node = mock(Node.class); + when(node.getId()).thenReturn(id); + when(node.getTargetObjectId()).thenReturn(id); + when(node.getChildNodes()).thenReturn(new ArrayList<>()); + when(node.getBorderNodes()).thenReturn(new ArrayList<>()); + return node; + } +} diff --git a/scripts/check-coverage.jsh b/scripts/check-coverage.jsh index dccff16c44..a0a60205a1 100755 --- a/scripts/check-coverage.jsh +++ b/scripts/check-coverage.jsh @@ -29,26 +29,26 @@ double checkCoverage(String module) { } record ModuleCoverage(String moduleName, double expectedCoverage) {} -double expectedGlobalCoverage = 70.0; +double expectedGlobalCoverage = 71.0; var moduleCoverageData = List.of( new ModuleCoverage("syson-application", 37.0), - new ModuleCoverage("syson-application-configuration", 77.0), + new ModuleCoverage("syson-application-configuration", 78.0), new ModuleCoverage("syson-common-view", 100.0), new ModuleCoverage("syson-diagram-common-view", 97.0), - new ModuleCoverage("syson-diagram-services", 82.0), + new ModuleCoverage("syson-diagram-services", 84.0), new ModuleCoverage("syson-direct-edit-grammar", 66.0), new ModuleCoverage("syson-form-services", 100.0), new ModuleCoverage("syson-model-services", 90.0), new ModuleCoverage("syson-representation-services", 100.0), - new ModuleCoverage("syson-services", 70.0), - new ModuleCoverage("syson-siriusweb-customnodes-metamodel", 41.0), + new ModuleCoverage("syson-services", 75.0), + new ModuleCoverage("syson-siriusweb-customnodes-metamodel", 48.0), new ModuleCoverage("syson-siriusweb-customnodes-metamodel-edit", 0.0), new ModuleCoverage("syson-standard-diagrams-view", 98.0), new ModuleCoverage("syson-sysml-export", 71.0), new ModuleCoverage("syson-sysml-import", 86.0), new ModuleCoverage("syson-sysml-metamodel", 76.0), new ModuleCoverage("syson-sysml-metamodel-edit", 18.0), - new ModuleCoverage("syson-sysml-metamodel-services", 74.0), + new ModuleCoverage("syson-sysml-metamodel-services", 75.0), new ModuleCoverage("syson-sysml-rest-api-services", 93.0), new ModuleCoverage("syson-sysml-validation", 97.0), new ModuleCoverage("syson-table-requirements-view", 79.0),