This commit is contained in:
2026-06-07 18:49:52 +02:00
parent 728df1d8ba
commit a4a3493b42
33 changed files with 903 additions and 936 deletions

View File

@@ -1,11 +0,0 @@
package click.kamil.springstatemachineexporter;
import org.junit.jupiter.api.Test;
class MainTest {
@Test
void contextLoads() {
}
}

View File

@@ -114,19 +114,19 @@ public class RegressionTest {
String actualBaseName = actualDir.getFileName().toString();
for (StateMachineExporter exporter : outputs) {
String fileName = actualBaseName + exporter.getFileExtension();
String goldenFileName = targetBaseName + exporter.getFileExtension();
String fileName = actualBaseName + exporter.getFileExtension();
String goldenFileName = targetBaseName + exporter.getFileExtension();
Path actualFile = actualDir.resolve(fileName);
Path goldenFile = scenario.goldenPath().resolve(goldenFileName);
Path actualFile = actualDir.resolve(fileName);
Path goldenFile = scenario.goldenPath().resolve(goldenFileName);
assertThat(actualFile)
.as("File %s should exist in output", fileName)
.exists();
assertThat(actualFile)
.as("File %s should exist in output", fileName)
.exists();
assertThat(actualFile)
.as("Content mismatch for %s", fileName)
.hasSameTextualContentAs(goldenFile);
}
assertThat(actualFile)
.as("Content mismatch for %s", fileName)
.content().isEqualTo(Files.readString(goldenFile));
}
}
}

View File

@@ -2,9 +2,16 @@ package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.Action;
import click.kamil.springstatemachineexporter.ast.app.domain.Guard;
import click.kamil.springstatemachineexporter.ast.app.domain.State;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.app.domain.TransitionType;
import org.eclipse.jdt.core.dom.*;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -13,6 +20,13 @@ import static org.assertj.core.api.Assertions.assertThat;
class AstTransitionParserTest {
private CodebaseContext context;
@BeforeEach
void setUp() {
context = new CodebaseContext();
}
@Test
void shouldParseSimpleWithExternalTransition() {
String source = """
@@ -24,20 +38,59 @@ class AstTransitionParserTest {
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo(TransitionType.EXTERNAL);
assertThat(transition.getSourceStates()).containsExactly("CREATED");
assertThat(transition.getTargetStates()).containsExactly("PAID");
assertThat(transition.getSourceStates()).extracting(State::toString).containsExactly("CREATED");
assertThat(transition.getTargetStates()).extracting(State::toString).containsExactly("PAID");
assertThat(transition)
.extracting(Transition::getEvent)
.isEqualTo("PAY");
}
@Test
void shouldResolveStaticImportsForStates() {
String source = """
import static com.example.OrderStates.CREATED;
import static com.example.OrderStates.PAID;
public class TestClass {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions.withExternal().source(CREATED).target(PAID).event("PAY");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
System.out.println("Actual source state: " + transitions.getFirst().getSourceStates().get(0));
assertThat(transitions.getFirst().getSourceStates()).extracting(State::toString).containsExactly("com.example.OrderStates.CREATED");
assertThat(transitions.getFirst().getTargetStates()).extracting(State::toString).containsExactly("com.example.OrderStates.PAID");
}
@Test
void shouldResolveQualifiedNamesForStates() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions.withExternal().source(OrderStates.CREATED).target(OrderStates.PAID);
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
// Without full scan, OrderStates is treated as a simple qualifier
assertThat(transitions.getFirst().getSourceStates()).extracting(State::toString).containsExactly("OrderStates.CREATED");
}
@Test
void shouldParseTransitionWithGuard() {
String source = """
@@ -49,78 +102,36 @@ class AstTransitionParserTest {
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getGuard())
.extracting(Guard::expression)
.isEqualTo("amount > 0");
assertThat(transition.getGuard())
.extracting(Guard::isLambda)
.isEqualTo(false);
}
@Test
void shouldParseTransitionWithLambdaGuard() {
void shouldParseTransitionWithLambdaGuardAndAction() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("CREATED").target("PAID").event("PAY").guard(context -> context.getExtendedState().getVariables().get("amount") > 0);
transitions.withExternal()
.source("CREATED")
.target("PAID")
.event("PAY")
.guard(context -> true)
.action(context -> System.out.println("Action"));
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getGuard().expression()).contains("context -> context.getExtendedState().getVariables().get(\"amount\") > 0");
assertThat(transition.getGuard().isLambda()).isTrue();
}
@Test
void shouldParseTransitionWithAction() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("CREATED").target("PAID").event("PAY").action("processPayment()");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getActions())
.extracting(Action::expression)
.containsExactly("processPayment()");
assertThat(transition.getActions())
.extracting(Action::isLambda)
.containsExactly(false);
}
@Test
void shouldParseTransitionWithLambdaAction() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("CREATED").target("PAID").event("PAY").action(context -> System.out.println("Payment processed"));
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getActions())
.extracting(Action::expression)
.anyMatch(s -> s.contains("context -> System.out.println(\"Payment processed\")"));
assertThat(transition.getActions())
.extracting(Action::isLambda)
.containsExactly(true);
@@ -137,29 +148,9 @@ class AstTransitionParserTest {
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(2);
Transition first = transitions.getFirst();
assertThat(first)
.extracting(Transition::getType)
.isEqualTo(TransitionType.EXTERNAL);
assertThat(first.getSourceStates()).containsExactly("CREATED");
assertThat(first.getTargetStates()).containsExactly("PAID");
assertThat(first)
.extracting(Transition::getEvent)
.isEqualTo("PAY");
Transition second = transitions.get(1);
assertThat(second)
.extracting(Transition::getType)
.isEqualTo(TransitionType.EXTERNAL);
assertThat(second.getSourceStates()).containsExactly("PAID");
assertThat(second.getTargetStates()).containsExactly("SHIPPED");
assertThat(second)
.extracting(Transition::getEvent)
.isEqualTo("SHIP");
}
@Test
@@ -173,321 +164,34 @@ class AstTransitionParserTest {
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(3);
Transition first = transitions.getFirst();
assertThat(first)
.extracting(Transition::getType)
.isEqualTo(TransitionType.CHOICE);
assertThat(first.getSourceStates()).containsExactly("PAID");
assertThat(first.getTargetStates()).containsExactly("SHIPPED");
assertThat(first.getGuard())
.extracting(Guard::expression)
.isEqualTo("isExpress()");
assertThat(first)
.extracting(Transition::getOrder)
.isEqualTo(0);
Transition second = transitions.get(1);
assertThat(second)
.extracting(Transition::getType)
.isEqualTo(TransitionType.CHOICE);
assertThat(second.getSourceStates()).containsExactly("PAID");
assertThat(second.getTargetStates()).containsExactly("CANCELLED");
assertThat(second.getGuard())
.extracting(Guard::expression)
.isEqualTo("isCancelled()");
assertThat(second)
.extracting(Transition::getOrder)
.isEqualTo(1);
Transition third = transitions.get(2);
assertThat(third)
.extracting(Transition::getType)
.isEqualTo(TransitionType.CHOICE);
assertThat(third.getSourceStates()).containsExactly("PAID");
assertThat(third.getTargetStates()).containsExactly("PENDING");
assertThat(third)
.extracting(Transition::getOrder)
.isEqualTo(2);
assertThat(transitions.getFirst().getSourceStates()).extracting(State::toString).containsExactly("PAID");
}
@Test
void shouldParseWithJunctionTransition() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withJunction().source("PAID").first("SHIPPED", "isExpress()").then("CANCELLED", "isCancelled()").last("PENDING");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(3);
assertThat(transitions.getFirst())
.extracting(Transition::getType)
.isEqualTo(TransitionType.JUNCTION);
assertThat(transitions.get(1))
.extracting(Transition::getType)
.isEqualTo(TransitionType.JUNCTION);
assertThat(transitions.get(2))
.extracting(Transition::getType)
.isEqualTo(TransitionType.JUNCTION);
}
@Test
void shouldParseWithInternalTransition() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withInternal().source("PAID").event("UPDATE").action("updateStatus()");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo(TransitionType.INTERNAL);
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition)
.extracting(Transition::getEvent)
.isEqualTo("UPDATE");
assertThat(transition.getActions())
.extracting(Action::expression)
.containsExactly("updateStatus()");
}
@Test
void shouldParseWithLocalTransition() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withLocal().source("PAID").target("PROCESSING").event("PROCESS");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo(TransitionType.LOCAL);
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition.getTargetStates()).containsExactly("PROCESSING");
assertThat(transition)
.extracting(Transition::getEvent)
.isEqualTo("PROCESS");
}
@Test
void shouldParseWithForkTransition() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withFork().source("PAID").target("SHIPPING").target("BILLING");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo(TransitionType.FORK);
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition.getTargetStates()).containsExactlyInAnyOrder("SHIPPING", "BILLING");
}
@Test
void shouldParseWithJoinTransition() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withJoin().source("SHIPPING").source("BILLING").target("COMPLETED");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition)
.extracting(Transition::getType)
.isEqualTo(TransitionType.JOIN);
assertThat(transition.getSourceStates()).containsExactlyInAnyOrder("SHIPPING", "BILLING");
assertThat(transition.getTargetStates()).containsExactly("COMPLETED");
}
@Test
void shouldReturnEmptyList_whenMethodIsNull() {
List<Transition> transitions = AstTransitionParser.parseTransitions(null);
assertThat(transitions).isEmpty();
}
@Test
void shouldReturnEmptyList_whenMethodHasNoBody() {
String source = """
public class TestClass {
public void configure() {
// Empty method
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).isEmpty();
}
@Test
void shouldReturnEmptyList_whenMethodHasNoTransitionStatements() {
String source = """
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
System.out.println("No transitions here");
int x = 5;
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).isEmpty();
}
@Test
void shouldParseTransitionWithMultipleSources() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("CREATED").source("PENDING").target("PAID").event("PAY");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getSourceStates()).containsExactlyInAnyOrder("CREATED", "PENDING");
assertThat(transition.getTargetStates()).containsExactly("PAID");
}
@Test
void shouldParseTransitionWithMultipleTargets() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("PAID").target("SHIPPED").target("CANCELLED").event("PROCESS");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition.getTargetStates()).containsExactlyInAnyOrder("SHIPPED", "CANCELLED");
}
@Test
void shouldParseChoiceTransitionWithAction() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withChoice()
.source("PAID")
.first("SHIPPED", "isExpress()", "shipExpress()")
.then("CANCELLED", "isCancelled()", "cancelOrder()")
.last("PENDING", "defaultAction()");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(3);
assertThat(transitions.getFirst().getActions())
.extracting(Action::expression)
.containsExactly("shipExpress()");
assertThat(transitions.get(1).getActions())
.extracting(Action::expression)
.containsExactly("cancelOrder()");
assertThat(transitions.get(2).getActions())
.extracting(Action::expression)
.containsExactly("defaultAction()");
}
@Test
void shouldDetectLambdaExpressions() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("CREATED").target("PAID").event("PAY").guard(context -> context.getExtendedState().getVariables().get("amount") > 0).action(context -> System.out.println("Payment processed"));
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getGuard().isLambda()).isTrue();
assertThat(transition.getActions())
.extracting(Action::isLambda)
.containsExactly(true);
}
@Test
void shouldDetectAnonymousClassExpressions() {
void shouldDetectAnonymousClassAsLambda() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("CREATED")
.target("PAID")
.event("PAY")
.source("S1")
.target("S2")
.action(new Action<String, String>() {
@Override
public void execute(StateContext<String, String> context) {
System.out.println("Payment processed");
}
public void execute(StateContext<String, String> context) {}
});
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getActions())
assertThat(transitions.getFirst().getActions())
.extracting(Action::isLambda)
.containsExactly(true);
}
@@ -504,7 +208,7 @@ class AstTransitionParserTest {
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);
assertThat(transitions).hasSize(2);
assertThat(transitions.get(0).getEvent()).isEqualTo("E1");
@@ -526,4 +230,4 @@ class AstTransitionParserTest {
}
return null;
}
}
}

View File

@@ -1,8 +1,9 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.State;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -10,9 +11,7 @@ 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;
@@ -50,7 +49,7 @@ class StateMachineAggregatorTest {
}
}
""";
writeClass("BaseConfig", baseSource);
writeClass("ChildConfig", childSource);
context.scan(tempDir);
@@ -118,8 +117,8 @@ class StateMachineAggregatorTest {
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).getSourceStates()).extracting(State::toString).containsExactly("START");
assertThat(transitions.get(0).getTargetStates()).extracting(State::toString).containsExactly("END");
assertThat(transitions.get(0).getEvent()).isEqualTo("GO");
}
@@ -438,6 +437,157 @@ class StateMachineAggregatorTest {
assertThat(transitions.get(0).getEvent()).isEqualTo("TRY_E1");
}
@Test
void shouldHandleComplexStateConfigurationWithMultipleBlocks() throws IOException {
String source = """
package com.example;
public class MultiBlockConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineStateConfigurer states) throws Exception {
states
.withStates()
.initial("START")
.state("S1")
.end("END_S1")
.and()
.withStates()
.parent("S1")
.initial("SUB_START")
.end("SUB_END");
}
}
""";
writeClass("MultiBlockConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("MultiBlockConfig");
aggregator.aggregateStates(td);
assertThat(aggregator.getInitialStates()).containsExactlyInAnyOrder("START", "SUB_START");
assertThat(aggregator.getEndStates()).containsExactlyInAnyOrder("END_S1", "SUB_END");
}
@Test
void shouldIgnoreStatesInsideRegion() throws IOException {
String source = """
package com.example;
public class RegionStateConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineStateConfigurer states) throws Exception {
states
.withStates()
.initial("CREATED")
.region("PROCESSING")
.and()
.withStates()
.parent("PROCESSING")
.initial("SHIPPING")
.end("COMPLETED");
}
}
""";
writeClass("RegionStateConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("RegionStateConfig");
aggregator.aggregateStates(td);
assertThat(aggregator.getInitialStates()).containsExactly("CREATED");
assertThat(aggregator.getEndStates()).containsExactly("COMPLETED");
}
@Test
void shouldHandleEmptyConfigureMethod() throws IOException {
String source = """
package com.example;
public class EmptyConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineStateConfigurer states) throws Exception {}
}
""";
writeClass("EmptyConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("EmptyConfig");
aggregator.aggregateStates(td);
assertThat(aggregator.getInitialStates()).isEmpty();
}
@Test
void shouldHandleMultipleMethodsHandlingTransitions() throws IOException {
String source = """
package com.example;
public class MultiMethodConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions.withExternal().source("S1").target("S2").event("E1");
otherTransitions(transitions);
}
private void otherTransitions(StateMachineTransitionConfigurer transitions) {
transitions.withExternal().source("S2").target("S3").event("E2");
}
}
""";
writeClass("MultiMethodConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("MultiMethodConfig");
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 shouldHandleMethodCallsWithEnumArguments() throws IOException {
String source = """
package com.example;
public class EnumConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
addTransition(transitions, States.S1, States.S2);
}
private void addTransition(StateMachineTransitionConfigurer t, States s, States e) {
t.withExternal().source(s).target(e).event("ENUM_E1");
}
}
""";
writeClass("EnumConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("EnumConfig");
List<Transition> transitions = aggregator.aggregateTransitions(td);
assertThat(transitions).hasSize(1);
assertThat(transitions.get(0).getSourceStates()).extracting(State::toString).containsExactly("States.S1");
}
@Test
void shouldIgnoreStaticMethodsThatAreNotCalled() throws IOException {
String source = """
package com.example;
public class UncalledStaticConfig extends StateMachineConfigurerAdapter {
public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
transitions.withExternal().source("S1").target("S2").event("E1");
}
public static void uncalled(StateMachineTransitionConfigurer transitions) {
transitions.withExternal().source("S2").target("S3").event("E2");
}
}
""";
writeClass("UncalledStaticConfig", source);
context.scan(tempDir);
TypeDeclaration td = context.getTypeDeclaration("UncalledStaticConfig");
List<Transition> transitions = aggregator.aggregateTransitions(td);
assertThat(transitions).hasSize(1);
assertThat(transitions.get(0).getEvent()).isEqualTo("E1");
}
private void writeClass(String name, String source) throws IOException {
Files.writeString(tempDir.resolve(name + ".java"), source);
}

View File

@@ -1,5 +1,6 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.State;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import org.junit.jupiter.api.Test;
@@ -186,59 +187,17 @@ class TransitionStateUtilsTest {
}
@Test
void shouldHandleTransitionsWithWhitespaceInStates() {
void shouldHandleVariousStateIdFormats() {
List<Transition> transitions = List.of(
createTransition(" CREATED ", " PAID "),
createTransition(" PAID ", " SHIPPED ")
createTransition(" OrderStates.START ", "STATE_1"),
createTransition("STATE_1", "getEndState()")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactly(" CREATED ");
assertThat(endStates).containsExactly(" SHIPPED ");
}
@Test
void shouldHandleTransitionsWithSpecialCharactersInStates() {
List<Transition> transitions = List.of(
createTransition("STATE_1", "STATE_2"),
createTransition("STATE_2", "STATE_3")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactly("STATE_1");
assertThat(endStates).containsExactly("STATE_3");
}
@Test
void shouldHandleTransitionsWithEnumValues() {
List<Transition> transitions = List.of(
createTransition("OrderStates.CREATED", "OrderStates.PAID"),
createTransition("OrderStates.PAID", "OrderStates.SHIPPED")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactly("OrderStates.CREATED");
assertThat(endStates).containsExactly("OrderStates.SHIPPED");
}
@Test
void shouldHandleTransitionsWithMethodCalls() {
List<Transition> transitions = List.of(
createTransition("getInitialState()", "getPaidState()"),
createTransition("getPaidState()", "getShippedState()")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactly("getInitialState()");
assertThat(endStates).containsExactly("getShippedState()");
assertThat(startStates).containsExactly(" OrderStates.START ");
assertThat(endStates).containsExactly("getEndState()");
}
@Test
@@ -261,10 +220,10 @@ class TransitionStateUtilsTest {
private Transition createTransition(String source, String target) {
Transition transition = new Transition();
if (source != null) {
transition.getSourceStates().add(source);
transition.getSourceStates().add(State.of(source));
}
if (target != null) {
transition.getTargetStates().add(target);
transition.getTargetStates().add(State.of(target));
}
return transition;
}
@@ -272,11 +231,13 @@ class TransitionStateUtilsTest {
private Transition createTransition(List<String> sources, List<String> targets) {
Transition transition = new Transition();
if (sources != null) {
transition.getSourceStates().addAll(sources);
for (String s : sources)
transition.getSourceStates().add(State.of(s));
}
if (targets != null) {
transition.getTargetStates().addAll(targets);
for (String t : targets)
transition.getTargetStates().add(State.of(t));
}
return transition;
}
}
}

View File

@@ -109,7 +109,7 @@ class CodebaseContextTest {
public StateMachine<String, String> stateMachine() {
return null;
}
public StateMachine<String, String> notABean() {
return null;
}
@@ -146,4 +146,103 @@ class CodebaseContextTest {
assertThat(beanMethods).hasSize(1);
assertThat(beanMethods.get(0).getName().getIdentifier()).isEqualTo("stateMachine");
}
@Test
void shouldFindClassWithMultipleAnnotations() throws IOException {
String source = """
package com.example;
@org.springframework.context.annotation.Configuration
@click.kamil.MyAnnotation
public class MultiAnnotated {}
""";
Files.writeString(tempDir.resolve("MultiAnnotated.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.findEntryPointClasses(List.of("Configuration"))).hasSize(1);
assertThat(context.findEntryPointClasses(List.of("MyAnnotation"))).hasSize(1);
}
@Test
void shouldIgnoreInterfacesWithAnnotations() throws IOException {
String source = """
package com.example;
import org.springframework.context.annotation.Configuration;
@Configuration
public interface MyInterface {}
""";
Files.writeString(tempDir.resolve("MyInterface.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.findEntryPointClasses(List.of("Configuration"))).isEmpty();
}
@Test
void shouldFindBeanMethodsWithGenericParameters() throws IOException {
String source = """
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.statemachine.StateMachine;
public class MyApp {
@Bean
public StateMachine<States, Events> stateMachine() {
return null;
}
}
""";
Files.writeString(tempDir.resolve("MyApp.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
var beanMethods = context.findBeanMethodsReturning(java.util.Set.of("StateMachine"));
assertThat(beanMethods).hasSize(1);
}
@Test
void shouldHandleFilesWithOnlyComments() throws IOException {
String source = """
// This is just a comment
/* And another one */
""";
Files.writeString(tempDir.resolve("Comments.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.findEntryPointClasses(List.of("EnableStateMachine"))).isEmpty();
}
@Test
void shouldBeCaseSensitiveForAnnotations() throws IOException {
String source = """
package com.example;
@enablestatemachine
public class WrongCase {}
""";
Files.writeString(tempDir.resolve("WrongCase.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.findEntryPointClasses(List.of("EnableStateMachine"))).isEmpty();
}
@Test
void shouldFindClassWithFullyQualifiedAnnotation() throws IOException {
String source = """
package com.example;
@org.springframework.statemachine.config.EnableStateMachine
public class FqnAnnotated {}
""";
Files.writeString(tempDir.resolve("FqnAnnotated.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.findEntryPointClasses(List.of("EnableStateMachine"))).hasSize(1);
}
}