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 styleSwitch = new SysMLCustomnodesSwitch<>() { + @Override + public String caseSysMLPackageNodeStyleDescription(SysMLPackageNodeStyleDescription object) { + return "package"; + } + + @Override + public String caseSysMLNoteNodeStyleDescription(SysMLNoteNodeStyleDescription object) { + return "note"; + } + + @Override + public String caseSysMLImportedPackageNodeStyleDescription(SysMLImportedPackageNodeStyleDescription object) { + return "imported-package"; + } + + @Override + public String caseSysMLViewFrameNodeStyleDescription(SysMLViewFrameNodeStyleDescription object) { + return "view-frame"; + } + }; + + assertThat(styleSwitch.doSwitch(SysMLCustomnodesFactory.eINSTANCE.createSysMLPackageNodeStyleDescription())).isEqualTo("package"); + assertThat(styleSwitch.doSwitch(SysMLCustomnodesFactory.eINSTANCE.createSysMLNoteNodeStyleDescription())).isEqualTo("note"); + assertThat(styleSwitch.doSwitch(SysMLCustomnodesFactory.eINSTANCE.createSysMLImportedPackageNodeStyleDescription())).isEqualTo("imported-package"); + assertThat(styleSwitch.doSwitch(SysMLCustomnodesFactory.eINSTANCE.createSysMLViewFrameNodeStyleDescription())).isEqualTo("view-frame"); + } + + /** + * Verifies the model adapter factory applicability and default adapter creation path. + */ + @DisplayName("GIVEN custom-node styles, WHEN adapting them, THEN the generated factory accepts every model object") + @Test + void testAdapterFactoryAcceptsAndAdaptsCustomNodeStyles() { + SysMLCustomnodesAdapterFactory adapterFactory = new SysMLCustomnodesAdapterFactory(); + + assertThat(adapterFactory.isFactoryForType(SysMLCustomnodesPackage.eINSTANCE)).isTrue(); + assertThat(adapterFactory.isFactoryForType(SysMLCustomnodesFactory.eINSTANCE.createSysMLPackageNodeStyleDescription())).isTrue(); + assertThat(adapterFactory.isFactoryForType(new Object())).isFalse(); + + assertThat(adapterFactory.createAdapter(SysMLCustomnodesFactory.eINSTANCE.createSysMLPackageNodeStyleDescription())).isNull(); + assertThat(adapterFactory.createAdapter(SysMLCustomnodesFactory.eINSTANCE.createSysMLNoteNodeStyleDescription())).isNull(); + assertThat(adapterFactory.createAdapter(SysMLCustomnodesFactory.eINSTANCE.createSysMLImportedPackageNodeStyleDescription())).isNull(); + assertThat(adapterFactory.createAdapter(SysMLCustomnodesFactory.eINSTANCE.createSysMLViewFrameNodeStyleDescription())).isNull(); + } + + /** + * Verifies classifier-based creation for every custom-node style and rejection of foreign EClasses. + */ + @DisplayName("GIVEN custom-node EClasses, WHEN creating from their classifier, THEN the matching style is created") + @Test + void testFactoryCreatesCustomNodeStylesFromEClass() { + assertThat(SysMLCustomnodesFactory.eINSTANCE.create(SysMLCustomnodesPackage.eINSTANCE.getSysMLPackageNodeStyleDescription())) + .isInstanceOf(SysMLPackageNodeStyleDescription.class); + assertThat(SysMLCustomnodesFactory.eINSTANCE.create(SysMLCustomnodesPackage.eINSTANCE.getSysMLNoteNodeStyleDescription())) + .isInstanceOf(SysMLNoteNodeStyleDescription.class); + assertThat(SysMLCustomnodesFactory.eINSTANCE.create(SysMLCustomnodesPackage.eINSTANCE.getSysMLImportedPackageNodeStyleDescription())) + .isInstanceOf(SysMLImportedPackageNodeStyleDescription.class); + assertThat(SysMLCustomnodesFactory.eINSTANCE.create(SysMLCustomnodesPackage.eINSTANCE.getSysMLViewFrameNodeStyleDescription())) + .isInstanceOf(SysMLViewFrameNodeStyleDescription.class); + assertThatIllegalArgumentException().isThrownBy(() -> SysMLCustomnodesFactory.eINSTANCE.create(EcoreFactory.eINSTANCE.createEClass())); + } + + /** + * Verifies that every custom style retains its configured background color. + */ + @DisplayName("GIVEN custom-node styles, WHEN assigning their backgrounds, THEN each style retains its own color") + @Test + void testCustomNodeStylesRetainBackgroundColors() { + FixedColor packageBackground = this.fixedColor("#112233"); + FixedColor noteBackground = this.fixedColor("#445566"); + FixedColor viewFrameBackground = this.fixedColor("#778899"); + SysMLPackageNodeStyleDescription packageStyle = SysMLCustomnodesFactory.eINSTANCE.createSysMLPackageNodeStyleDescription(); + SysMLNoteNodeStyleDescription noteStyle = SysMLCustomnodesFactory.eINSTANCE.createSysMLNoteNodeStyleDescription(); + SysMLViewFrameNodeStyleDescription viewFrameStyle = SysMLCustomnodesFactory.eINSTANCE.createSysMLViewFrameNodeStyleDescription(); + + packageStyle.setBackground(packageBackground); + noteStyle.setBackground(noteBackground); + viewFrameStyle.setBackground(viewFrameBackground); + + assertThat(packageStyle.getBackground()).isSameAs(packageBackground); + assertThat(noteStyle.getBackground()).isSameAs(noteBackground); + assertThat(viewFrameStyle.getBackground()).isSameAs(viewFrameBackground); + } + + /** + * Creates a fixed color used by a custom-node style test. + * + * @param value + * the hexadecimal color value + * @return the configured fixed color + */ + private FixedColor fixedColor(String value) { + FixedColor color = ViewFactory.eINSTANCE.createFixedColor(); + color.setValue(value); + return color; + } +} diff --git a/backend/services/syson-diagram-services/src/test/java/org/eclipse/syson/diagram/services/DiagramQueryToolServiceTest.java b/backend/services/syson-diagram-services/src/test/java/org/eclipse/syson/diagram/services/DiagramQueryToolServiceTest.java new file mode 100644 index 0000000000..7e4b8bc3fa --- /dev/null +++ b/backend/services/syson-diagram-services/src/test/java/org/eclipse/syson/diagram/services/DiagramQueryToolServiceTest.java @@ -0,0 +1,229 @@ +/******************************************************************************* + * 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.diagram.services; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.emf.common.util.BasicEList; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.components.collaborative.diagrams.DiagramContext; +import org.eclipse.sirius.components.diagrams.Diagram; +import org.eclipse.sirius.components.diagrams.Node; +import org.eclipse.syson.sysml.AcceptActionUsage; +import org.eclipse.syson.sysml.Expression; +import org.eclipse.syson.sysml.FeatureTyping; +import org.eclipse.syson.sysml.Membership; +import org.eclipse.syson.sysml.ReferenceUsage; +import org.eclipse.syson.sysml.Relationship; +import org.eclipse.syson.sysml.RequirementUsage; +import org.eclipse.syson.sysml.StateSubactionMembership; +import org.eclipse.syson.sysml.StateUsage; +import org.eclipse.syson.sysml.SysmlFactory; +import org.eclipse.syson.sysml.Type; +import org.eclipse.syson.sysml.UseCaseUsage; +import org.eclipse.syson.sysml.ViewDefinition; +import org.eclipse.syson.sysml.ViewUsage; +import org.eclipse.syson.util.StandardDiagramsConstants; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link DiagramQueryToolService}'s diagram-tool visibility and compartment-query decisions. + *

+ * 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(List.of(receiverMembership))); + assertThat(service.isEmptyAcceptActionUsageReceiver(acceptActionUsage)).isTrue(); + receiverMembership.setMemberElement(SysmlFactory.eINSTANCE.createPartUsage()); + assertThat(service.isEmptyAcceptActionUsageReceiver(acceptActionUsage)).isFalse(); + + ReferenceUsage payloadParameter = mock(ReferenceUsage.class); + FeatureTyping featureTyping = mock(FeatureTyping.class); + when(acceptActionUsage.getPayloadParameter()).thenReturn(payloadParameter); + when(payloadParameter.getOwnedRelationship()).thenReturn(new BasicEList(List.of(featureTyping))); + assertThat(service.isEmptyAcceptActionUsagePayload(acceptActionUsage)).isTrue(); + when(featureTyping.getType()).thenReturn(SysmlFactory.eINSTANCE.createPartDefinition()); + assertThat(service.isEmptyAcceptActionUsagePayload(acceptActionUsage)).isFalse(); + } + + @DisplayName("GIVEN inherited-members options, WHEN they are disabled, THEN no inherited feature is returned") + @Test + void testGetInheritedCompartmentItemsWithDisabledOptions() { + IObjectSearchService objectSearchService = mock(IObjectSearchService.class); + ShowDiagramsInheritedMembersService inheritedMembersService = new ShowDiagramsInheritedMembersService(); + inheritedMembersService.setShowInheritedMembers(false); + inheritedMembersService.setShowInheritedMembersFromStandardLibraries(false); + DiagramQueryToolService service = new DiagramQueryToolService(objectSearchService, inheritedMembersService); + Type type = SysmlFactory.eINSTANCE.createPartDefinition(); + + assertThat(service.getInheritedCompartmentItems(type, "ownedFeature")).isEmpty(); + } + + @DisplayName("GIVEN standard view kinds, WHEN checking tool availability, THEN each view enforces its semantic context rules") + @Test + void testToolShouldBeAvailableForStandardViews() { + DiagramQueryToolService service = this.createService(mock(IObjectSearchService.class)); + + assertThat(service.toolShouldBeAvailable(SysmlFactory.eINSTANCE.createPackage(), mock(IEditingContext.class), mock(DiagramContext.class), + org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getPartUsage())).isTrue(); + assertThat(service.toolShouldBeAvailable(this.viewUsage(StandardDiagramsConstants.IV_QN, SysmlFactory.eINSTANCE.createPackage()), mock(IEditingContext.class), + mock(DiagramContext.class), org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getAttributeUsage())).isFalse(); + assertThat(service.toolShouldBeAvailable(this.viewUsage(StandardDiagramsConstants.IV_QN, SysmlFactory.eINSTANCE.createPackage()), mock(IEditingContext.class), + mock(DiagramContext.class), org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getPartUsage())).isTrue(); + assertThat(service.toolShouldBeAvailable(this.viewUsage(StandardDiagramsConstants.AFV_QN, SysmlFactory.eINSTANCE.createActionUsage()), mock(IEditingContext.class), + mock(DiagramContext.class), org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getPartUsage())).isTrue(); + assertThat(service.toolShouldBeAvailable(this.viewUsage(StandardDiagramsConstants.AFV_QN, SysmlFactory.eINSTANCE.createActionDefinition()), mock(IEditingContext.class), + mock(DiagramContext.class), org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getPartUsage())).isTrue(); + assertThat(service.toolShouldBeAvailable(this.viewUsage(StandardDiagramsConstants.AFV_QN, SysmlFactory.eINSTANCE.createPartUsage()), mock(IEditingContext.class), + mock(DiagramContext.class), org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getActionDefinition())).isTrue(); + assertThat(service.toolShouldBeAvailable(this.viewUsage(StandardDiagramsConstants.STV_QN, SysmlFactory.eINSTANCE.createStateUsage()), mock(IEditingContext.class), + mock(DiagramContext.class), org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getPartUsage())).isTrue(); + assertThat(service.toolShouldBeAvailable(this.viewUsage(StandardDiagramsConstants.STV_QN, SysmlFactory.eINSTANCE.createStateDefinition()), mock(IEditingContext.class), + mock(DiagramContext.class), org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getPartUsage())).isTrue(); + assertThat(service.toolShouldBeAvailable(this.viewUsage(StandardDiagramsConstants.STV_QN, SysmlFactory.eINSTANCE.createPartUsage()), mock(IEditingContext.class), + mock(DiagramContext.class), org.eclipse.syson.sysml.SysmlPackage.eINSTANCE.getStateDefinition())).isTrue(); + } + + @DisplayName("GIVEN an Action Flow View diagram, WHEN checking a background control-node tool, THEN its ViewUsage owner determines availability") + @Test + void testIsControlNodeActionCreationToolInsideActionOnAFV() { + IObjectSearchService objectSearchService = mock(IObjectSearchService.class); + IEditingContext editingContext = mock(IEditingContext.class); + Diagram diagram = mock(Diagram.class); + DiagramContext diagramContext = mock(DiagramContext.class); + when(diagramContext.diagram()).thenReturn(diagram); + when(diagram.getTargetObjectId()).thenReturn("diagram-target"); + ViewUsage actionFlowViewUsage = this.actionFlowViewUsage(); + when(objectSearchService.getObject(editingContext, "diagram-target")).thenReturn(Optional.of(actionFlowViewUsage)); + DiagramQueryToolService service = this.createService(objectSearchService); + + assertThat(service.isControlNodeActionCreationToolInsideActionOnAFV(this.viewUsage(StandardDiagramsConstants.AFV_QN, SysmlFactory.eINSTANCE.createActionUsage()), + editingContext, diagramContext)).isTrue(); + assertThat(service.isControlNodeActionCreationToolInsideActionOnAFV(this.viewUsage(StandardDiagramsConstants.AFV_QN, SysmlFactory.eINSTANCE.createPartUsage()), + editingContext, diagramContext)).isFalse(); + when(objectSearchService.getObject(editingContext, "diagram-target")).thenReturn(Optional.empty()); + assertThat(service.isControlNodeActionCreationToolInsideActionOnAFV(SysmlFactory.eINSTANCE.createActionUsage(), editingContext, diagramContext)).isFalse(); + } + + private DiagramQueryToolService createService(IObjectSearchService objectSearchService) { + return new DiagramQueryToolService(objectSearchService, new ShowDiagramsInheritedMembersService()); + } + + private ViewUsage actionFlowViewUsage() { + ViewDefinition viewDefinition = mock(ViewDefinition.class); + when(viewDefinition.getQualifiedName()).thenReturn(StandardDiagramsConstants.AFV_QN); + ViewUsage viewUsage = mock(ViewUsage.class); + when(viewUsage.getType()).thenReturn(new BasicEList(List.of(viewDefinition))); + return viewUsage; + } + + private ViewUsage viewUsage(String qualifiedName, org.eclipse.syson.sysml.Element owner) { + Type type = mock(Type.class); + when(type.getQualifiedName()).thenReturn(qualifiedName); + ViewUsage viewUsage = mock(ViewUsage.class); + when(viewUsage.getType()).thenReturn(new BasicEList(List.of(type))); + when(viewUsage.getOwner()).thenReturn(owner); + return viewUsage; + } +} diff --git a/backend/services/syson-services/src/test/java/org/eclipse/syson/services/RelatedElementsSwitchTest.java b/backend/services/syson-services/src/test/java/org/eclipse/syson/services/RelatedElementsSwitchTest.java new file mode 100644 index 0000000000..64aa5048ee --- /dev/null +++ b/backend/services/syson-services/src/test/java/org/eclipse/syson/services/RelatedElementsSwitchTest.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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 org.eclipse.syson.sysml.Annotation; +import org.eclipse.syson.sysml.Dependency; +import org.eclipse.syson.sysml.FeatureTyping; +import org.eclipse.syson.sysml.Membership; +import org.eclipse.syson.sysml.Redefinition; +import org.eclipse.syson.sysml.ReferenceSubsetting; +import org.eclipse.syson.sysml.Subclassification; +import org.eclipse.syson.sysml.Subsetting; +import org.eclipse.syson.sysml.SysmlFactory; +import org.eclipse.syson.sysml.SysmlPackage; +import org.eclipse.syson.sysml.Succession; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link RelatedElementsSwitch}'s relationship-deletion rules. + *

+ * 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),