ai tests + bug fix on last with 2args + strip quotes feature

This commit is contained in:
2025-07-20 07:33:28 +02:00
parent a4d0f817bc
commit b485bdd784
8 changed files with 1524 additions and 21 deletions

View File

@@ -77,7 +77,7 @@ public class AstTransitionParser {
// Extract source state once
for (MethodInvocation call : segment) {
if ("source".equals(call.getName().getIdentifier()) && !call.arguments().isEmpty()) {
sourceState = call.arguments().getFirst().toString();
sourceState = QuotedExpression.of(call.arguments().getFirst()).toStringWithoutQuotes();
break; // only take the first occurrence
}
}
@@ -93,11 +93,17 @@ public class AstTransitionParser {
if (sourceState != null) {
t.getSourceStates().add(sourceState);
}
t.getTargetStates().add(args.get(0).toString());
t.getTargetStates().add(QuotedExpression.of(args.get(0)).toStringWithoutQuotes());
if (args.size() > 1) {
parseGuard(args.get(1), t);
if ("last".equals(methodName)) {
// For 'last', the second argument is an action, not a guard
parseAction(args.get(1), t);
} else {
// For 'first' and 'then', the second argument is a guard
parseGuard(args.get(1), t);
}
}
if ("then".equals(methodName) && args.size() > 2) {
if (args.size() > 2) {
parseAction(args.get(2), t);
}
t.setOrder(orderCounter++);
@@ -122,9 +128,9 @@ public class AstTransitionParser {
}
switch (methodName) {
case "source" -> args.forEach(arg -> t.getSourceStates().add(arg.toString()));
case "target" -> args.forEach(arg -> t.getTargetStates().add(arg.toString()));
case "event" -> t.setEvent(args.getFirst().toString());
case "source" -> args.forEach(arg -> t.getSourceStates().add(QuotedExpression.of(arg).toStringWithoutQuotes()));
case "target" -> args.forEach(arg -> t.getTargetStates().add(QuotedExpression.of(arg).toStringWithoutQuotes()));
case "event" -> t.setEvent(QuotedExpression.of(args.getFirst()).toStringWithoutQuotes());
case "guard" -> parseGuard(args.getFirst(), t);
case "action" -> parseAction(args.getFirst(), t);
}
@@ -133,16 +139,18 @@ public class AstTransitionParser {
}
private static void parseGuard(Object arg, Transition t) {
if (arg instanceof Expression expr) {
t.setGuard(expr.toString());
t.setLambdaGuard(isLambdaOrAnonymous(expr));
QuotedExpression quotedExpr = QuotedExpression.of(arg);
if (quotedExpr != null) {
t.setGuard(quotedExpr.toStringWithoutQuotes());
t.setLambdaGuard(isLambdaOrAnonymous(quotedExpr.getExpression()));
}
}
private static void parseAction(Object arg, Transition t) {
if (arg instanceof Expression expr) {
t.getActions().add(expr.toString());
t.getIsLambdaActions().add(isLambdaOrAnonymous(expr));
QuotedExpression quotedExpr = QuotedExpression.of(arg);
if (quotedExpr != null) {
t.getActions().add(quotedExpr.toStringWithoutQuotes());
t.getIsLambdaActions().add(isLambdaOrAnonymous(quotedExpr.getExpression()));
}
}

View File

@@ -0,0 +1,34 @@
package click.kamil.springstatemachineexporter.ast.app;
import lombok.Getter;
import org.eclipse.jdt.core.dom.Expression;
@Getter
public class QuotedExpression {
private final Expression expression;
private QuotedExpression(Expression expression) {
this.expression = expression;
}
public String toStringWithoutQuotes() {
if (expression == null) return null;
return stripQuotes(expression.toString());
}
public static String stripQuotes(String s) {
if (s == null) return null;
return s.replaceAll("^\"|\"$", "");
}
public static QuotedExpression of(Expression expression) {
return new QuotedExpression(expression);
}
public static QuotedExpression of(Object obj) {
if (obj instanceof Expression expr) {
return new QuotedExpression(expr);
}
return null;
}
}

View File

@@ -94,10 +94,10 @@ public class StateMachineStateConfigurationMethodFinder {
switch (methodName) {
case "initial" -> {
if (!isInsideRegionOrFork(call)) {
initialStates.add(arg.toString());
initialStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
}
}
case "end" -> endStates.add(arg.toString());
case "end" -> endStates.add(QuotedExpression.of(arg).toStringWithoutQuotes());
// ignore others
}
}

View File

@@ -31,12 +31,7 @@ public class TransitionStateUtils {
private static Stream<String> normalizeStates(Collection<String> states) {
return states.stream()
.map(TransitionStateUtils::stripQuotes)
.map(QuotedExpression::stripQuotes)
.filter(Objects::nonNull);
}
private static String stripQuotes(String s) {
if (s == null) return null;
return s.replaceAll("^\"|\"$", "");
}
}

View File

@@ -0,0 +1,438 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import org.eclipse.jdt.core.dom.*;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class AstTransitionParserTest {
@Test
void shouldParseSimpleWithExternalTransition() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("CREATED").target("PAID").event("PAY");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getType()).isEqualTo("withExternal");
assertThat(transition.getSourceStates()).containsExactly("CREATED");
assertThat(transition.getTargetStates()).containsExactly("PAID");
assertThat(transition.getEvent()).isEqualTo("PAY");
}
@Test
void shouldParseTransitionWithGuard() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("CREATED").target("PAID").event("PAY").guard("amount > 0");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getGuard()).isEqualTo("amount > 0");
assertThat(transition.isLambdaGuard()).isFalse();
}
@Test
void shouldParseTransitionWithLambdaGuard() {
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);
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(1);
Transition transition = transitions.getFirst();
assertThat(transition.getGuard()).contains("context -> context.getExtendedState().getVariables().get(\"amount\") > 0");
assertThat(transition.isLambdaGuard()).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()).containsExactly("processPayment()");
assertThat(transition.getIsLambdaActions()).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()).hasSize(1);
assertThat(transition.getActions().getFirst()).contains("context -> System.out.println(\"Payment processed\")");
assertThat(transition.getIsLambdaActions()).containsExactly(true);
}
@Test
void shouldParseMultipleTransitionsWithAnd() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal().source("CREATED").target("PAID").event("PAY").and().withExternal().source("PAID").target("SHIPPED").event("SHIP");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(2);
Transition first = transitions.getFirst();
assertThat(first.getType()).isEqualTo("withExternal");
assertThat(first.getSourceStates()).containsExactly("CREATED");
assertThat(first.getTargetStates()).containsExactly("PAID");
assertThat(first.getEvent()).isEqualTo("PAY");
Transition second = transitions.get(1);
assertThat(second.getType()).isEqualTo("withExternal");
assertThat(second.getSourceStates()).containsExactly("PAID");
assertThat(second.getTargetStates()).containsExactly("SHIPPED");
assertThat(second.getEvent()).isEqualTo("SHIP");
}
@Test
void shouldParseWithChoiceTransition() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withChoice().source("PAID").first("SHIPPED", "isExpress()").then("CANCELLED", "isCancelled()").last("PENDING");
}
}
""";
MethodDeclaration method = createMethodDeclaration(source);
List<Transition> transitions = AstTransitionParser.parseTransitions(method);
assertThat(transitions).hasSize(3);
Transition first = transitions.getFirst();
assertThat(first.getType()).isEqualTo("withChoice");
assertThat(first.getSourceStates()).containsExactly("PAID");
assertThat(first.getTargetStates()).containsExactly("SHIPPED");
assertThat(first.getGuard()).isEqualTo("isExpress()");
assertThat(first.getOrder()).isEqualTo(0);
Transition second = transitions.get(1);
assertThat(second.getType()).isEqualTo("withChoice");
assertThat(second.getSourceStates()).containsExactly("PAID");
assertThat(second.getTargetStates()).containsExactly("CANCELLED");
assertThat(second.getGuard()).isEqualTo("isCancelled()");
assertThat(second.getOrder()).isEqualTo(1);
Transition third = transitions.get(2);
assertThat(third.getType()).isEqualTo("withChoice");
assertThat(third.getSourceStates()).containsExactly("PAID");
assertThat(third.getTargetStates()).containsExactly("PENDING");
assertThat(third.getOrder()).isEqualTo(2);
}
@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().getType()).isEqualTo("withJunction");
assertThat(transitions.get(1).getType()).isEqualTo("withJunction");
assertThat(transitions.get(2).getType()).isEqualTo("withJunction");
}
@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.getType()).isEqualTo("withInternal");
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition.getEvent()).isEqualTo("UPDATE");
assertThat(transition.getActions()).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.getType()).isEqualTo("withLocal");
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition.getTargetStates()).containsExactly("PROCESSING");
assertThat(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.getType()).isEqualTo("withFork");
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.getType()).isEqualTo("withJoin");
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()).containsExactly("shipExpress()");
assertThat(transitions.get(1).getActions()).containsExactly("cancelOrder()");
assertThat(transitions.get(2).getActions()).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.isLambdaGuard()).isTrue();
assertThat(transition.getIsLambdaActions()).containsExactly(true);
}
@Test
void shouldDetectAnonymousClassExpressions() {
String source = """
public class TestClass {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("CREATED")
.target("PAID")
.event("PAY")
.action(new Action<String, String>() {
@Override
public void execute(StateContext<String, String> 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.getIsLambdaActions()).containsExactly(true);
}
private MethodDeclaration createMethodDeclaration(String source) {
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setSource(source.toCharArray());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
MethodDeclaration[] methods = typeDecl.getMethods();
if (methods.length > 0) {
return methods[0];
}
return null;
}
}

View File

@@ -0,0 +1,392 @@
package click.kamil.springstatemachineexporter.ast.app;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class StateMachineStateConfigurationMethodFinderTest {
@Test
void shouldParseInitialStates() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldParseMultipleInitialStates() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.initial(OrderStates.PENDING)
.end(OrderStates.COMPLETED)
.end(OrderStates.CANCELLED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PENDING");
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.CANCELLED");
}
@Test
void shouldIgnoreStatesInsideFork() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.fork(OrderStates.PROCESSING)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.initial(OrderStates.SHIPPING)
.initial(OrderStates.BILLING)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldIgnoreStatesInsideRegion() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.region(OrderStates.PROCESSING)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.initial(OrderStates.SHIPPING)
.initial(OrderStates.BILLING)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldParseStatesWithStringLiterals() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("CREATED")
.end("COMPLETED");
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("CREATED");
assertThat(visitor.getEndStates()).containsExactly("COMPLETED");
}
@Test
void shouldParseStatesWithQuotedStrings() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("CREATED")
.end("COMPLETED");
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("CREATED");
assertThat(visitor.getEndStates()).containsExactly("COMPLETED");
}
@Test
void shouldIgnoreNonConfigureMethods() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
public void someOtherMethod() {
states
.withStates()
.initial(OrderStates.CREATED);
}
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.PENDING)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.PENDING");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldHandleEmptyConfigureMethod() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
// Empty method
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).isEmpty();
assertThat(visitor.getEndStates()).isEmpty();
}
@Test
void shouldHandleConfigureMethodWithoutWithStates() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
System.out.println("No state configuration here");
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).isEmpty();
assertThat(visitor.getEndStates()).isEmpty();
}
@Test
void shouldParseComplexStateConfiguration() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.state(OrderStates.PAID)
.state(OrderStates.SHIPPED)
.end(OrderStates.COMPLETED)
.end(OrderStates.CANCELLED)
.and()
.withStates()
.parent(OrderStates.PAID)
.initial(OrderStates.PROCESSING)
.end(OrderStates.READY);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PROCESSING");
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.CANCELLED", "OrderStates.READY");
}
@Test
void shouldParseStatesWithMethodCalls() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(getInitialState())
.end(getEndState());
}
private OrderStates getInitialState() {
return OrderStates.CREATED;
}
private OrderStates getEndState() {
return OrderStates.COMPLETED;
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("getInitialState()");
assertThat(visitor.getEndStates()).containsExactly("getEndState()");
}
@Test
void shouldHandleMultipleConfigureMethods() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.end(OrderStates.COMPLETED);
}
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// This should be ignored
states
.withStates()
.initial(OrderStates.PENDING);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactlyInAnyOrder("OrderStates.CREATED", "OrderStates.PENDING");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldParseStatesWithEnumValues() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.state(OrderStates.PAID)
.state(OrderStates.SHIPPED)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactly("OrderStates.COMPLETED");
}
@Test
void shouldHandleNestedForkAndRegion() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
states
.withStates()
.initial(OrderStates.CREATED)
.fork(OrderStates.PROCESSING)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.region(OrderStates.SHIPPING)
.and()
.withStates()
.parent(OrderStates.SHIPPING)
.initial(OrderStates.PREPARING)
.end(OrderStates.SHIPPED)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.region(OrderStates.BILLING)
.and()
.withStates()
.parent(OrderStates.BILLING)
.initial(OrderStates.CALCULATING)
.end(OrderStates.BILLED)
.and()
.withStates()
.parent(OrderStates.PROCESSING)
.end(OrderStates.COMPLETED);
}
}
""";
CompilationUnit cu = StateMachineStateConfigurationMethodFinder.parse(source);
StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor visitor =
new StateMachineStateConfigurationMethodFinder.StateConfigurerVisitor();
cu.accept(visitor);
assertThat(visitor.getInitialStates()).containsExactly("OrderStates.CREATED");
assertThat(visitor.getEndStates()).containsExactlyInAnyOrder("OrderStates.COMPLETED", "OrderStates.SHIPPED", "OrderStates.BILLED");
}
}

View File

@@ -0,0 +1,344 @@
package click.kamil.springstatemachineexporter.ast.app;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class StateMachineTransitionConfigurationMethodFinderTest {
@Test
void shouldFindConfigureMethodInEnumStateMachineConfigurerAdapter() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
transitions
.withExternal()
.source(OrderStates.CREATED)
.target(OrderStates.PAID)
.event(OrderEvents.PAY);
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldFindConfigureMethodInStateMachineConfigurerAdapter() {
String source = """
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("CREATED")
.target("PAID")
.event("PAY");
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldReturnNull_whenClassDoesNotExtendStateMachineConfigurer() {
String source = """
public class OrderStateMachineConfig {
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenClassIsInterface() {
String source = """
public interface OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception;
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodIsNotPublic() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
protected void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodDoesNotReturnVoid() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public String configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// This should not be found
return "result";
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodHasWrongNumberOfParameters() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure() throws Exception {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodParameterIsNotStateMachineTransitionConfigurer() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodDoesNotThrowException() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldReturnNull_whenConfigureMethodThrowsDifferentException() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws RuntimeException {
// This should not be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldFindConfigureMethodWithMultipleExceptions() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception, RuntimeException {
transitions
.withExternal()
.source(OrderStates.CREATED)
.target(OrderStates.PAID)
.event(OrderEvents.PAY);
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldFindConfigureMethodWithParameterizedType() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
transitions
.withExternal()
.source(OrderStates.CREATED)
.target(OrderStates.PAID)
.event(OrderEvents.PAY);
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldReturnNull_whenMultipleConfigureMethodsExist() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
// State configuration
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// Transition configuration
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldHandleClassWithNoMethods() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
// No methods
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method).isNull();
}
@Test
void shouldHandleClassWithOtherMethods() {
String source = """
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
public void someOtherMethod() {
// This should be ignored
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// This should be found
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldHandleClassWithGenericParameters() {
String source = """
public class OrderStateMachineConfig<S, E> extends EnumStateMachineConfigurerAdapter<S, E> {
@Override
public void configure(StateMachineTransitionConfigurer<S, E> transitions) throws Exception {
// Generic configuration
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
@Test
void shouldHandleClassWithAnnotations() {
String source = """
@Configuration
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {
@Override
@Bean
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
// Configuration with annotations
}
}
""";
StateMachineTransitionConfigurationMethodFinder finder = new StateMachineTransitionConfigurationMethodFinder(source);
MethodDeclaration method = finder.findConfigureMethod();
assertThat(method)
.extracting(MethodDeclaration::getName)
.extracting(SimpleName::getIdentifier)
.isEqualTo("configure");
}
}

View File

@@ -0,0 +1,292 @@
package click.kamil.springstatemachineexporter.ast.app;
import click.kamil.springstatemachineexporter.ast.app.domain.Transition;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class TransitionStateUtilsTest {
@Test
void shouldFindStartStates_whenStatesHaveNoIncomingTransitions() {
List<Transition> transitions = List.of(
createTransition("CREATED", "PAID"),
createTransition("PAID", "SHIPPED"),
createTransition("SHIPPED", "COMPLETED")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
assertThat(startStates).containsExactly("CREATED");
}
@Test
void shouldFindStartStates_whenMultipleStatesHaveNoIncomingTransitions() {
List<Transition> transitions = List.of(
createTransition("CREATED", "PAID"),
createTransition("PENDING", "PAID"),
createTransition("PAID", "SHIPPED")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
assertThat(startStates).containsExactlyInAnyOrder("CREATED", "PENDING");
}
@Test
void shouldFindEndStates_whenStatesHaveNoOutgoingTransitions() {
List<Transition> transitions = List.of(
createTransition("CREATED", "PAID"),
createTransition("PAID", "SHIPPED"),
createTransition("PAID", "CANCELLED")
);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(endStates).containsExactlyInAnyOrder("SHIPPED", "CANCELLED");
}
@Test
void shouldFindEndStates_whenMultipleStatesHaveNoOutgoingTransitions() {
List<Transition> transitions = List.of(
createTransition("CREATED", "PAID"),
createTransition("PAID", "SHIPPED"),
createTransition("PAID", "CANCELLED"),
createTransition("PAID", "REFUNDED")
);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(endStates).containsExactlyInAnyOrder("SHIPPED", "CANCELLED", "REFUNDED");
}
@Test
void shouldHandleEmptyTransitionsList() {
Set<String> startStates = TransitionStateUtils.findStartStates(List.of());
Set<String> endStates = TransitionStateUtils.findEndStates(List.of());
assertThat(startStates).isEmpty();
assertThat(endStates).isEmpty();
}
@Test
void shouldThrowException_whenNullTransitionsList() {
List<Transition> transitions = null;
assertThatThrownBy(() -> TransitionStateUtils.findStartStates(transitions))
.isInstanceOf(NullPointerException.class);
assertThatThrownBy(() -> TransitionStateUtils.findEndStates(transitions))
.isInstanceOf(NullPointerException.class);
}
@Test
void shouldHandleTransitionsWithQuotedStates() {
List<Transition> transitions = List.of(
createTransition("\"CREATED\"", "\"PAID\""),
createTransition("\"PAID\"", "\"SHIPPED\"")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactly("CREATED");
assertThat(endStates).containsExactly("SHIPPED");
}
@Test
void shouldHandleTransitionsWithMixedQuotedAndUnquotedStates() {
List<Transition> transitions = List.of(
createTransition("CREATED", "\"PAID\""),
createTransition("\"PAID\"", "SHIPPED")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactly("CREATED");
assertThat(endStates).containsExactly("SHIPPED");
}
@Test
void shouldHandleTransitionsWithNullStates() {
List<Transition> transitions = List.of(
createTransition(null, "PAID"),
createTransition("PAID", null)
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).isEmpty();
assertThat(endStates).isEmpty();
}
@Test
void shouldHandleTransitionsWithEmptyStates() {
List<Transition> transitions = List.of(
createTransition("", "PAID"),
createTransition("PAID", "")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).isEmpty();
assertThat(endStates).isEmpty();
}
@Test
void shouldHandleTransitionsWithMultipleSourceStates() {
Transition transition1 = createTransition(List.of("CREATED", "PENDING"), List.of("PAID"));
Transition transition2 = createTransition(List.of("PAID"), List.of("SHIPPED"));
List<Transition> transitions = List.of(transition1, transition2);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactlyInAnyOrder("CREATED", "PENDING");
assertThat(endStates).containsExactly("SHIPPED");
}
@Test
void shouldHandleTransitionsWithMultipleTargetStates() {
Transition transition1 = createTransition(
List.of("PAID"),
List.of("SHIPPED", "CANCELLED")
);
List<Transition> transitions = List.of(transition1);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactly("PAID");
assertThat(endStates).containsExactlyInAnyOrder("SHIPPED", "CANCELLED");
}
@Test
void shouldHandleCircularTransitions() {
List<Transition> transitions = List.of(
createTransition("A", "B"),
createTransition("B", "C"),
createTransition("C", "A")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).isEmpty();
assertThat(endStates).isEmpty();
}
@Test
void shouldHandleSelfLoopingTransitions() {
List<Transition> transitions = List.of(
createTransition("A", "A"),
createTransition("A", "B")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).isEmpty(); // A is both source and target, so not a start state
assertThat(endStates).containsExactly("B"); // Only B is a pure end state
}
@Test
void shouldHandleTransitionsWithWhitespaceInStates() {
List<Transition> transitions = List.of(
createTransition(" CREATED ", " PAID "),
createTransition(" PAID ", " SHIPPED ")
);
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()");
}
@Test
void shouldHandleComplexStateMachine() {
List<Transition> transitions = List.of(
createTransition("CREATED", "PAID"),
createTransition("PAID", "SHIPPED"),
createTransition("PAID", "CANCELLED"),
createTransition("SHIPPED", "DELIVERED"),
createTransition("CANCELLED", "REFUNDED")
);
Set<String> startStates = TransitionStateUtils.findStartStates(transitions);
Set<String> endStates = TransitionStateUtils.findEndStates(transitions);
assertThat(startStates).containsExactly("CREATED");
assertThat(endStates).containsExactlyInAnyOrder("DELIVERED", "REFUNDED");
}
private Transition createTransition(String source, String target) {
Transition transition = new Transition();
if (source != null) {
transition.getSourceStates().add(source);
}
if (target != null) {
transition.getTargetStates().add(target);
}
return transition;
}
private Transition createTransition(List<String> sources, List<String> targets) {
Transition transition = new Transition();
if (sources != null) {
transition.getSourceStates().addAll(sources);
}
if (targets != null) {
transition.getTargetStates().addAll(targets);
}
return transition;
}
}