Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
= Changelog

== v2026.5.0 (work in progress)
== v2026.7.0 (work in progress)

=== New features

- https://github.com/eclipse-syson/syson/issues/2112[#2112] [Diagram] Add tools to create Start and Done states, available on `StateUsage` and `StateDefinition` graphical nodes.

== v2026.5.0

=== Shapes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*******************************************************************************
* 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.eclipse.sirius.components.diagrams.tests.DiagramEventPayloadConsumer.assertRefreshedDiagramThat;

import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Stream;

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.Diagram;
import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionInput;
import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionSuccessPayload;
import org.eclipse.sirius.components.graphql.tests.api.IExecuteEditingContextFunctionRunner;
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.controllers.diagrams.checkers.CheckDiagramElementCount;
import org.eclipse.syson.application.controllers.diagrams.testers.ToolTester;
import org.eclipse.syson.application.controllers.utils.TestNameGenerator;
import org.eclipse.syson.application.data.GeneralViewWithTopNodesTestProjectData;
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.Package;
import org.eclipse.syson.sysml.SysmlPackage;
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.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
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;

/**
* Tests the New Start State and New Done State tools.
*
* @author Jerome Gout
*/
@Transactional
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GVNewStartDoneStatesTests extends AbstractIntegrationTests {

private final IDescriptionNameGenerator descriptionNameGenerator = new SDVDescriptionNameGenerator();

@Autowired
private IGivenInitialServerState givenInitialServerState;

@Autowired
private IGivenDiagramDescription givenDiagramDescription;

@Autowired
private IGivenDiagramSubscription givenDiagramSubscription;

@Autowired
private IDiagramIdProvider diagramIdProvider;

@Autowired
private ToolTester nodeCreationTester;

@Autowired
private DiagramComparator diagramComparator;

@Autowired
private IExecuteEditingContextFunctionRunner executeEditingContextFunctionRunner;

@Autowired
private IObjectSearchService objectSearchService;

private Flux<DiagramRefreshedEventPayload> 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();
}

private static Stream<Arguments> toolParameters() {
return Stream.of(Arguments.of("New Start State"), Arguments.of("New Done State")).map(TestNameGenerator::namedArguments);
}

@DisplayName("GIVEN a SysML Project with a StateUsage inside a package named States, WHEN invoking $toolName, THEN the state is added in the state transition compartment")
@GivenSysONServer({ GeneralViewWithTopNodesTestProjectData.SCRIPT_PATH })
@ParameterizedTest
@MethodSource("toolParameters")
public void checkNewStartDoneStateInStateUsage(String toolName) {
var diagramDescription = this.givenDiagramDescription.getDiagramDescription(GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID,
SysONRepresentationDescriptionIdentifiers.GENERAL_VIEW_DIAGRAM_DESCRIPTION_ID);
var diagramDescriptionIdProvider = new DiagramDescriptionIdProvider(diagramDescription, this.diagramIdProvider);
String creationToolId = diagramDescriptionIdProvider.getNodeToolId(this.descriptionNameGenerator.getNodeName(SysmlPackage.eINSTANCE.getStateUsage()), toolName);
AtomicReference<Diagram> diagram = new AtomicReference<>();

var flux = this.givenSubscriptionToDiagram();

Consumer<Object> initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set);

Runnable renamePackageAction = () -> {
var input = new ExecuteEditingContextFunctionInput(UUID.randomUUID(), GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID,
(editingContext, executeEditingContextFunctionInput) -> {
Optional<Object> optPackage = this.objectSearchService.getObject(editingContext, GeneralViewWithTopNodesTestProjectData.SemanticIds.PACKAGE_1_ID);
assertThat(optPackage).isPresent().get().isInstanceOf(Package.class);
var parentPackage = (Package) optPackage.get();
parentPackage.setDeclaredName("States");
return new ExecuteEditingContextFunctionSuccessPayload(executeEditingContextFunctionInput.id(), true);
});
var payload = this.executeEditingContextFunctionRunner.execute(input).block();
assertThat(payload).isInstanceOf(ExecuteEditingContextFunctionSuccessPayload.class);
};

Runnable invokeCreationTool = () -> this.nodeCreationTester.invokeTool(GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, diagram, GeneralViewWithTopNodesTestProjectData.SemanticIds.STATE_USAGE_ID, creationToolId);

Consumer<Object> diagramCheck = assertRefreshedDiagramThat(newDiagram -> {
new CheckDiagramElementCount(this.diagramComparator)
.hasNewNodeCount(1)
.check(diagram.get(), newDiagram);
});

StepVerifier.create(flux)
.consumeNextWith(initialDiagramContentConsumer)
.then(renamePackageAction)
.then(invokeCreationTool)
.consumeNextWith(diagramCheck)
.thenCancel()
.verify(Duration.ofSeconds(10));
}

@DisplayName("GIVEN a SysML Project with a StateDefinition inside a package named States, WHEN invoking $toolName, THEN the state is added in the state transition compartment")
@GivenSysONServer({ GeneralViewWithTopNodesTestProjectData.SCRIPT_PATH })
@ParameterizedTest
@MethodSource("toolParameters")
public void checkNewStartDoneStateInStateDefinition(String toolName) {
var diagramDescription = this.givenDiagramDescription.getDiagramDescription(GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID,
SysONRepresentationDescriptionIdentifiers.GENERAL_VIEW_DIAGRAM_DESCRIPTION_ID);
var diagramDescriptionIdProvider = new DiagramDescriptionIdProvider(diagramDescription, this.diagramIdProvider);
String creationToolId = diagramDescriptionIdProvider.getNodeToolId(this.descriptionNameGenerator.getNodeName(SysmlPackage.eINSTANCE.getStateDefinition()), toolName);
AtomicReference<Diagram> diagram = new AtomicReference<>();

var flux = this.givenSubscriptionToDiagram();

Consumer<Object> initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set);

Runnable renamePackageAction = () -> {
var input = new ExecuteEditingContextFunctionInput(UUID.randomUUID(), GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID,
(editingContext, executeEditingContextFunctionInput) -> {
Optional<Object> optPackage = this.objectSearchService.getObject(editingContext, GeneralViewWithTopNodesTestProjectData.SemanticIds.PACKAGE_1_ID);
assertThat(optPackage).isPresent().get().isInstanceOf(Package.class);
var parentPackage = (Package) optPackage.get();
parentPackage.setDeclaredName("States");
return new ExecuteEditingContextFunctionSuccessPayload(executeEditingContextFunctionInput.id(), true);
});
var payload = this.executeEditingContextFunctionRunner.execute(input).block();
assertThat(payload).isInstanceOf(ExecuteEditingContextFunctionSuccessPayload.class);
};

Runnable invokeCreationTool = () -> this.nodeCreationTester.invokeTool(GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, diagram, GeneralViewWithTopNodesTestProjectData.SemanticIds.STATE_DEFINITION_ID , creationToolId);

Consumer<Object> diagramCheck = assertRefreshedDiagramThat(newDiagram -> {
new CheckDiagramElementCount(this.diagramComparator)
.hasNewNodeCount(1)
.check(diagram.get(), newDiagram);
});

StepVerifier.create(flux)
.consumeNextWith(initialDiagramContentConsumer)
.then(renamePackageAction)
.then(invokeCreationTool)
.consumeNextWith(diagramCheck)
.thenCancel()
.verify(Duration.ofSeconds(10));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,28 @@ public List<StateUsage> getAllNonExhibitStates(Element element) {
*
* @param eObject
* the {@link EObject} stored in a {@link ResourceSet}
* @param type
* @param eClass
* the searched type, represented by its qualified name
* @return a list of reachable object
*/
public List<EObject> getAllReachable(EObject eObject, EClass eClass) {
return this.getAllReachableType(eObject, eClass);
}

/**
* * Get all reachable elements of the type given by the {@link EClass} in the {@link ResourceSet} of the given type
* (represented by its qualified name) without considering elements inside standard libs.
*
* @param eObject
* the {@link EObject} stored in a {@link ResourceSet}
* @param eClass
* the searched type, represented by its qualified name
* @return a list of reachable object
*/
public List<EObject> getAllReachableWithoutStandardLibs(EObject eObject, EClass eClass) {
return this.getAllReachableType(eObject, eClass, false);
}

/**
* Get all reachable elements of the type given by the {@link EClass} in the {@link ResourceSet} of the given
* {@link EObject}.
Expand Down Expand Up @@ -605,6 +619,18 @@ public ActionUsage retrieveStandardStartAction(Element eObject) {
return this.findByNameAndTypeInStandardLibraries(eObject, ActionUsage.class, "Actions::Action::start");
}

/**
* Retrieve the start state defined inside the standard library <code>States</code>.
*
* @param eObject
* an object to access to the library resources.
*
* @return the standard start StateUsage defined in the <code>States</code> library.
*/
public StateUsage retrieveStandardStartState(Element eObject) {
return this.findByNameAndTypeInStandardLibraries(eObject, StateUsage.class, "States::StateAction::start");
}

/**
* Retrieve the done action defined inside the standard library <code>Actions</code>.
*
Expand All @@ -617,6 +643,18 @@ public ActionUsage retrieveStandardDoneAction(Element eObject) {
return this.findByNameAndTypeInStandardLibraries(eObject, ActionUsage.class, "Actions::Action::done");
}

/**
* Retrieve the done state defined inside the standard library <code>States</code>.
*
* @param eObject
* an object to access to the library resources.
*
* @return the standard done StateUsage defined in the <code>States</code> library.
*/
public ActionUsage retrieveStandardDoneState(Element eObject) {
return this.findByNameAndTypeInStandardLibraries(eObject, ActionUsage.class, "States::StateAction::done");
}

private <T extends Element> T findByNameAndTypeInStandardLibraries(Element context, Class<T> klass, String qualifiedName) {
return context.eResource().getResourceSet().getResources().stream()
.flatMap(resource -> this.getLibraries(resource, true).stream())
Expand Down Expand Up @@ -1022,6 +1060,10 @@ public boolean isUnsynchronized(Element element) {
isUnsynchronized = true;
} else if (Objects.equals(element, this.retrieveStandardDoneAction(element))) {
isUnsynchronized = true;
} else if (Objects.equals(element, this.retrieveStandardStartState(element))) {
isUnsynchronized = true;
} else if (Objects.equals(element, this.retrieveStandardDoneState(element))) {
isUnsynchronized = true;
} else if (element instanceof NamespaceImport) {
isUnsynchronized = true;
} else if (element instanceof AnnotatingElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import org.eclipse.sirius.components.view.diagram.NodeTool;
import org.eclipse.sirius.components.view.diagram.SynchronizationPolicy;
import org.eclipse.syson.diagram.common.view.nodes.DoneActionNodeDescriptionProvider;
import org.eclipse.syson.diagram.common.view.nodes.DoneStateNodeDescriptionProvider;
import org.eclipse.syson.diagram.common.view.nodes.StartActionNodeDescriptionProvider;
import org.eclipse.syson.diagram.common.view.nodes.StartStateNodeDescriptionProvider;
import org.eclipse.syson.diagram.common.view.services.ViewEdgeService;
import org.eclipse.syson.diagram.services.aql.DiagramQueryAQLService;
import org.eclipse.syson.services.UtilService;
Expand Down Expand Up @@ -73,7 +75,7 @@ public EdgeDescription create() {
.name(this.getEdgeDescriptionName())
.preconditionExpression(ServiceMethod.of2(ViewEdgeService::isInSameGraphicalContainer).aql(org.eclipse.sirius.components.diagrams.description.EdgeDescription.GRAPHICAL_EDGE_SOURCE,
org.eclipse.sirius.components.diagrams.description.EdgeDescription.GRAPHICAL_EDGE_TARGET, org.eclipse.sirius.components.diagrams.description.DiagramDescription.CACHE))
.semanticCandidatesExpression(ServiceMethod.of1(UtilService::getAllReachable).aqlSelf(domainType))
.semanticCandidatesExpression(ServiceMethod.of1(UtilService::getAllReachableWithoutStandardLibs).aqlSelf(domainType))
.sourceExpression(AQLConstants.AQL_SELF + "." + SysmlPackage.eINSTANCE.getTransitionUsage_Source().getName())
.style(this.createDefaultEdgeStyle())
.conditionalStyles(this.createStateConditionalStyle())
Expand Down Expand Up @@ -181,11 +183,13 @@ protected List<NodeDescription> getAllActionOrStateUsage(IViewDiagramElementFind
}

protected boolean isStartNode(NodeDescription n) {
return Objects.equals(this.getDescriptionNameGenerator().getNodeName(StartActionNodeDescriptionProvider.START_ACTION_NAME), n.getName());
return Objects.equals(this.getDescriptionNameGenerator().getNodeName(StartActionNodeDescriptionProvider.START_ACTION_NAME), n.getName()) ||
Objects.equals(this.getDescriptionNameGenerator().getNodeName(StartStateNodeDescriptionProvider.START_STATE_NAME), n.getName());
}

protected boolean isDoneNode(NodeDescription n) {
return Objects.equals(this.getDescriptionNameGenerator().getNodeName(DoneActionNodeDescriptionProvider.DONE_ACTION_NAME), n.getName());
return Objects.equals(this.getDescriptionNameGenerator().getNodeName(DoneActionNodeDescriptionProvider.DONE_ACTION_NAME), n.getName()) ||
Objects.equals(this.getDescriptionNameGenerator().getNodeName(DoneStateNodeDescriptionProvider.DONE_STATE_NAME), n.getName());
}

protected boolean isActionOrStateUsage(NodeDescription nodeDescription) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023, 2025 Obeo.
* Copyright (c) 2023, 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
Expand Down Expand Up @@ -124,5 +124,9 @@ protected void addReusableCustomNodes(IViewDiagramElementFinder cache, List<Node
.ifPresent(childrenNodes::add);
cache.getNodeDescription(this.descriptionNameGenerator.getNodeName(DoneActionNodeDescriptionProvider.DONE_ACTION_NAME))
.ifPresent(childrenNodes::add);
cache.getNodeDescription(this.descriptionNameGenerator.getNodeName(StartStateNodeDescriptionProvider.START_STATE_NAME))
.ifPresent(childrenNodes::add);
cache.getNodeDescription(this.descriptionNameGenerator.getNodeName(DoneStateNodeDescriptionProvider.DONE_STATE_NAME))
.ifPresent(childrenNodes::add);
}
}
Loading
Loading