This commit is contained in:
2026-06-06 20:58:34 +02:00
parent 38f960dc55
commit 3c70d75d92
8 changed files with 568 additions and 161 deletions

View File

@@ -47,6 +47,12 @@ class RegressionTest {
Path.of("../state_machines/inheritance_state_machine"),
Path.of("src/test/resources/golden/OneStateMachineConfiguration"),
"OneStateMachineConfiguration"
),
new TestScenario(
"Two State Machine (Extra Functions)",
Path.of("../state_machines/inheritance_extra_functions_state_machine"),
Path.of("src/test/resources/golden/TwoStateMachineExtraFunctionsConfiguration"),
"TwoStateMachineConfiguration"
)
);
}

View File

@@ -468,6 +468,25 @@ class AstTransitionParserTest {
assertThat(transition.getIsLambdaActions()).containsExactly(true);
}
@Test
void shouldParseTransitionsWithVariableDeclarations() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
ExternalTransitionConfigurer<String, String> t1 = transitions.withExternal().source("S1").target("S2").event("E1");
t1.and().withExternal().source("S2").target("S3").event("E2");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(2);
assertThat(transitions.get(0).getEvent()).isEqualTo("E1");
assertThat(transitions.get(1).getEvent()).isEqualTo("E2");
}
private MethodDeclaration createMethodDeclaration(String source) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
@@ -483,4 +502,4 @@ class AstTransitionParserTest {
}
return null;
}
}
}

View File

@@ -0,0 +1,282 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class StateMachineAggregatorTest {
@TempDir
Path tempDir;
private CodebaseContext context;
private StateMachineAggregator aggregator;
@BeforeEach
void setUp() {
context = new CodebaseContext();
aggregator = new StateMachineAggregator(context);
}
@Test
void shouldAggregateTransitionsFromInheritance() throws IOException {
String baseSource = """
package com.example;
public abstract class BaseConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions.withExternal().source("BASE_S1").target("BASE_S2").event("BASE_E1");
}
}
""";
String childSource = """
package com.example;
public class ChildConfig extends BaseConfig {
@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
super.configure(transitions);
transitions.withExternal().source("CHILD_S1").target("CHILD_S2").event("CHILD_E1");
}
}
""";
writeClass("BaseConfig", baseSource);
writeClass("ChildConfig", childSource);
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig");
List<Transition> transitions = aggregator.aggregateTransitions(childTd);
assertThat(transitions).hasSize(2);
assertThat(transitions).anyMatch(t -> t.getEvent().equals("BASE_E1"));
assertThat(transitions).anyMatch(t -> t.getEvent().equals("CHILD_E1"));
}
@Test
void shouldFollowMethodCallsAcrossHierarchy() throws IOException {
String baseSource = """
package com.example;
public abstract class BaseConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
addBaseTransitions(transitions);
}
protected void addBaseTransitions(StateMachineTransitionConfigurer transitions) {
transitions.withExternal().source("BASE_S1").target("BASE_S2").event("BASE_E1");
}
}
""";
String childSource = """
package com.example;
public class ChildConfig extends BaseConfig {
@Override
protected void addBaseTransitions(StateMachineTransitionConfigurer transitions) {
transitions.withExternal().source("OVERRIDDEN_S1").target("OVERRIDDEN_S2").event("OVERRIDDEN_E1");
}
}
""";
writeClass("BaseConfig", baseSource);
writeClass("ChildConfig", childSource);
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig");
List<Transition> transitions = aggregator.aggregateTransitions(childTd);
assertThat(transitions).hasSize(1);
assertThat(transitions.get(0).getEvent()).isEqualTo("OVERRIDDEN_E1");
}
@Test
void shouldResolveParametersInMethodCalls() throws IOException {
String source = """
package com.example;
public class ParamConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
addCustomTransition(transitions, "START", "END", "GO");
}
private void addCustomTransition(StateMachineTransitionConfigurer t, String src, String tgt, String evt) {
t.withExternal().source(src).target(tgt).event(evt);
}
}
""";
writeClass("ParamConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("ParamConfig");
List<Transition> transitions = aggregator.aggregateTransitions(td);
assertThat(transitions).hasSize(1);
assertThat(transitions.get(0).getSourceStates()).containsExactly("START");
assertThat(transitions.get(0).getTargetStates()).containsExactly("END");
assertThat(transitions.get(0).getEvent()).isEqualTo("GO");
}
@Test
void shouldParseStreamOfForEachTransitions() throws IOException {
String source = """
package com.example;
import java.util.stream.Stream;
public class StreamConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
Stream.of("E1", "E2").forEach(e -> {
transitions.withExternal().source("S1").target("S2").event(e);
});
}
}
""";
writeClass("StreamConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("StreamConfig");
List<Transition> transitions = aggregator.aggregateTransitions(td);
assertThat(transitions).hasSize(2);
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E1"));
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E2"));
}
@Test
void shouldAggregateStatesAcrossInheritance() throws IOException {
String baseSource = """
package com.example;
public abstract class BaseConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineStateConfigurer states) throws Exception {
states.withStates().initial("BASE_START");
}
}
""";
String childSource = """
package com.example;
public class ChildConfig extends BaseConfig {
public void configure(StateMachineStateConfigurer states) throws Exception {
super.configure(states);
states.withStates().state("CHILD_STATE").end("CHILD_END");
}
}
""";
writeClass("BaseConfig", baseSource);
writeClass("ChildConfig", childSource);
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig");
Set<String> initialStates = new HashSet<>();
Set<String> endStates = new HashSet<>();
aggregator.aggregateStates(childTd, initialStates, endStates);
assertThat(initialStates).containsExactly("BASE_START");
assertThat(endStates).containsExactly("CHILD_END");
}
@Test
void shouldParseStreamOfForEachStates() throws IOException {
String source = """
package com.example;
import java.util.stream.Stream;
public class StreamStateConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineStateConfigurer states) throws Exception {
Stream.of("S1", "S2").forEach(s -> {
states.withStates().initial(s);
});
}
}
""";
writeClass("StreamStateConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("StreamStateConfig");
Set<String> initialStates = new HashSet<>();
Set<String> endStates = new HashSet<>();
aggregator.aggregateStates(td, initialStates, endStates);
assertThat(initialStates).containsExactlyInAnyOrder("S1", "S2");
}
@Test
void shouldAggregateTransitionsFromDeepInheritance() throws IOException {
String grandparentSource = """
package com.example;
public abstract class GrandparentConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions.withExternal().source("GP_S1").target("GP_S2").event("GP_E1");
}
}
""";
String parentSource = """
package com.example;
public abstract class ParentConfig extends GrandparentConfig {
@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
super.configure(transitions);
transitions.withExternal().source("P_S1").target("P_S2").event("P_E1");
}
}
""";
String childSource = """
package com.example;
public class ChildConfig extends ParentConfig {
@Override
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
super.configure(transitions);
transitions.withExternal().source("C_S1").target("C_S2").event("C_E1");
}
}
""";
writeClass("GrandparentConfig", grandparentSource);
writeClass("ParentConfig", parentSource);
writeClass("ChildConfig", childSource);
context.scan(tempDir);
TypeDeclaration childTd = context.getTypeDeclaration("ChildConfig");
List<Transition> transitions = aggregator.aggregateTransitions(childTd);
assertThat(transitions).hasSize(3);
assertThat(transitions).anyMatch(t -> t.getEvent().equals("GP_E1"));
assertThat(transitions).anyMatch(t -> t.getEvent().equals("P_E1"));
assertThat(transitions).anyMatch(t -> t.getEvent().equals("C_E1"));
}
@Test
void shouldParseEnhancedForLoopTransitions() throws IOException {
String source = """
package com.example;
import java.util.List;
public class LoopConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
for (String e : List.of("E1", "E2")) {
transitions.withExternal().source("S1").target("S2").event(e);
}
}
}
""";
writeClass("LoopConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("LoopConfig");
List<Transition> transitions = aggregator.aggregateTransitions(td);
assertThat(transitions).hasSize(2);
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E1"));
assertThat(transitions).anyMatch(t -> t.getEvent().equals("E2"));
}
private void writeClass(String name, String source) throws IOException {
Files.writeString(tempDir.resolve(name + ".java"), source);
}
}