spring-projects / spring-statemachine

Spring Statemachine is a framework for application developers to use state machine concepts with Spring.
1.51k stars 598 forks source link

UmlStateMachineModelFactory - inconsistency between StateMachineModel and StateMachine - duplicated transitions created! #1141

Open pdalfarr opened 4 months ago

pdalfarr commented 4 months ago

Using simple-root-regions.uml to perform some tests, I encountered an unexpected situation where the 'transitions per regions' are not consistent between StateMachineModel and StateMachine.

I am using the simple-root-regions.uml file from this repository to create a StateMachineModel.

This StateMachineModel contains:

When I create a StateMachine based on this StateMachineModel, I end up with a StateMachine containing:

So it seems the "S1->S2" from Region1 has been duplicated in Regon2 as illustrated below.

Unit test available :-)

I created a unit test avalable here to help you troubleshot the issue. This unit test does compare a StateMachineModel and a StateMachine regarding:

simple-root-regions

pdalfarr commented 4 months ago

Adding the unit test in this ticket.

So the last assertion does fail:

assertThat(transitionsOfRegion2InStateMachine).isEqualTo(transitionsOfRegion2InStateMachineModel);

Unit test to add in UmlStateMachineModelFactoryTests.java :

    public void testStateMachineVsStateMachineModelConsistency() {
        context.refresh();
        Resource model1 = new ClassPathResource("org/springframework/statemachine/uml/simple-root-regions.uml");
        UmlStateMachineModelFactory builder = new UmlStateMachineModelFactory(model1);
        builder.setBeanFactory(context);
        assertThat(model1.exists()).isTrue();
        StateMachineModel<String, String> stateMachineModel = builder.build();

        try {
            // build statemachine from model
            UmlStateMachineModelFactory umlStateMachineModelFactory = new UmlStateMachineModelFactory(("classpath:org/springframework/statemachine/uml/simple-root-regions.uml"));
            StateMachineBuilder.Builder<String, String> stateMachineBuilder = StateMachineBuilder.builder();
            stateMachineBuilder.configureModel().withModel().factory(umlStateMachineModelFactory);
            stateMachineBuilder.configureConfiguration().withConfiguration();
            StateMachine<String, String> stateMachine = stateMachineBuilder.build();

            // get the "root" state of this state machines
            State<String, String> rootState = stateMachine.getStates().stream().findFirst().get();
            assertThat(rootState).isInstanceOf(RegionState.class);
            RegionState<String, String> rootRegionState = ((RegionState<String, String>) rootState);

            // compare statemachine and stateMachineModel

            // states in Region1
            AbstractStateMachine region1InStatemachine = (AbstractStateMachine)
                    ((List) rootRegionState.getRegions()).stream()
                            .filter(region -> ((AbstractStateMachine) region).getId().contains("Region1"))
                            .findFirst().get();

            List statesOfRegion1InStateMachine = region1InStatemachine.getStates().stream()
                    .map(o -> ((State) o).getId().toString())
                    .sorted().toList();

            List<String> statesOfRegion1InStateMachineModel = stateMachineModel.getStatesData().getStateData().stream()
                    .filter(stateData -> "Region1".equals(stateData.getRegion().toString()))
                    .map(stateData -> stateData.getState().toString())
                    .sorted().toList();

            assertThat(statesOfRegion1InStateMachine).isEqualTo(statesOfRegion1InStateMachineModel);

            // states in Region2
            AbstractStateMachine region2InStatemachine = (AbstractStateMachine)
                    ((List) rootRegionState.getRegions()).stream()
                            .filter(region -> ((AbstractStateMachine) region).getId().contains("Region2"))
                            .findFirst().get();

            List statesOfRegion2InStateMachine = region2InStatemachine.getStates().stream()
                    .map(o -> ((State) o).getId().toString())
                    .sorted().toList();

            List<String> statesOfRegion2InStateMachineModel = stateMachineModel.getStatesData().getStateData().stream()
                    .filter(stateData -> "Region2".equals(stateData.getRegion().toString()))
                    .map(stateData -> stateData.getState().toString())
                    .sorted().toList();

            assertThat(statesOfRegion2InStateMachine).isEqualTo(statesOfRegion2InStateMachineModel);

            // transitions in Region1
            List transitionsOfRegion1InStateMachine = region1InStatemachine.getTransitions().stream()
                    .map(o -> ((Transition) o).getSource().getId().toString() + "->" + ((Transition) o).getTarget().getId().toString())
                    .sorted().toList();

            List<String> transitionsOfRegion1InStateMachineModel = stateMachineModel.getTransitionsData().getTransitions().stream()
                    // let's exclude "initial" transition
                    .filter(transitionData -> !transitionData.getSource().startsWith("initial"))
                    .filter(transitionData -> statesOfRegion1InStateMachine.contains(transitionData.getSource())
                            || statesOfRegion1InStateMachine.contains(transitionData.getTarget()))
                    .map(transitionData -> transitionData.getSource() + "->" + transitionData.getTarget())
                    .sorted().toList();

            assertThat(transitionsOfRegion1InStateMachine).isEqualTo(transitionsOfRegion1InStateMachineModel);

            // transitions in Region2
            List transitionsOfRegion2InStateMachine = region2InStatemachine.getTransitions().stream()
                    .map(o -> ((Transition) o).getSource().getId().toString() + "->" + ((Transition) o).getTarget().getId().toString())
                    .sorted().toList();

            List<String> transitionsOfRegion2InStateMachineModel = stateMachineModel.getTransitionsData().getTransitions().stream()
                    // let's exclude "initial" transition
                    .filter(transitionData -> !transitionData.getSource().startsWith("initial"))
                    .filter(transitionData -> statesOfRegion2InStateMachine.contains(transitionData.getSource())
                            || statesOfRegion2InStateMachine.contains(transitionData.getTarget()))
                    .map(transitionData -> transitionData.getSource() + "->" + transitionData.getTarget())
                    .sorted().toList();

            // WOW! this is failing! Why is transition "S1->S2" present in both Region1 AND Region2 ?!?
            // Does this indicates an issue in UmlStateMachineModelFactory ???
            // Expected :["S1->S2"]
            // Actual   :["S1->S2", "S3->S4"]
            assertThat(transitionsOfRegion2InStateMachine).isEqualTo(transitionsOfRegion2InStateMachineModel);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }