move files into modules

This commit is contained in:
2026-06-06 15:07:03 +02:00
parent 00161b294c
commit cc66b7517b
88 changed files with 1090 additions and 79 deletions

View File

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

View File

@@ -0,0 +1,75 @@
package click.kamil.springstatemachineexporter;
import click.kamil.springstatemachineexporter.ast.out.Dot;
import click.kamil.springstatemachineexporter.ast.out.PlantUml;
import click.kamil.springstatemachineexporter.ast.out.Scxml;
import click.kamil.springstatemachineexporter.ast.out.StateMachineExporter;
import click.kamil.springstatemachineexporter.utils.TestScenario;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class RegressionTest {
static List<TestScenario> provideTestScenarios() {
return List.of(
new TestScenario(
"Complex State Machine",
Path.of("../state_machines/complex_state_machine"),
Path.of("src/test/resources/golden/ComplexStateMachineConfig"),
"ComplexStateMachineConfig"
),
new TestScenario(
"Fork Join State Machine",
Path.of("../state_machines/forkjoin_state_machine"),
Path.of("src/test/resources/golden/ForkJoinStateMachineConfig"),
"ForkJoinStateMachineConfig"
),
new TestScenario(
"Kamil Enum State Machine",
Path.of("../state_machines/enumstate_state_machine"),
Path.of("src/test/resources/golden/KamilEnumStateMachineConfiguration"),
"KamilEnumStateMachineConfiguration"
),
new TestScenario(
"Simple Enum Join State Machine",
Path.of("../state_machines/simple_state_machine"),
Path.of("src/test/resources/golden/SimpleEnumStateMachineConfiguration"),
"SimpleEnumStateMachineConfiguration"
)
);
}
@ParameterizedTest(name = "{0}")
@MethodSource("provideTestScenarios")
void testOutputMatchesGolden(TestScenario scenario, @TempDir Path tempDir) throws Exception {
Main.runExporter(scenario.inputPath(), tempDir);
List<StateMachineExporter> exporters = List.of(
new PlantUml(),
new Dot(),
new Scxml()
);
String baseName = scenario.baseName();
for (StateMachineExporter exporter : exporters) {
String fileName = baseName + exporter.getFileExtension();
Path actualFile = tempDir.resolve(baseName).resolve(fileName);
Path goldenFile = scenario.goldenPath().resolve(fileName);
assertThat(actualFile)
.as("File %s should exist in output", fileName)
.exists();
assertThat(actualFile)
.as("Content mismatch for %s", fileName)
.hasSameTextualContentAs(goldenFile);
}
}
}

View File

@@ -0,0 +1,486 @@
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)
.extracting(Transition::getType)
.isEqualTo("withExternal");
assertThat(transition.getSourceStates()).containsExactly("CREATED");
assertThat(transition.getTargetStates()).containsExactly("PAID");
assertThat(transition)
.extracting(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)
.extracting(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)
.extracting(Transition::getType)
.isEqualTo("withExternal");
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("withExternal");
assertThat(second.getSourceStates()).containsExactly("PAID");
assertThat(second.getTargetStates()).containsExactly("SHIPPED");
assertThat(second)
.extracting(Transition::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)
.extracting(Transition::getType)
.isEqualTo("withChoice");
assertThat(first.getSourceStates()).containsExactly("PAID");
assertThat(first.getTargetStates()).containsExactly("SHIPPED");
assertThat(first)
.extracting(Transition::getGuard)
.isEqualTo("isExpress()");
assertThat(first)
.extracting(Transition::getOrder)
.isEqualTo(0);
Transition second = transitions.get(1);
assertThat(second)
.extracting(Transition::getType)
.isEqualTo("withChoice");
assertThat(second.getSourceStates()).containsExactly("PAID");
assertThat(second.getTargetStates()).containsExactly("CANCELLED");
assertThat(second)
.extracting(Transition::getGuard)
.isEqualTo("isCancelled()");
assertThat(second)
.extracting(Transition::getOrder)
.isEqualTo(1);
Transition third = transitions.get(2);
assertThat(third)
.extracting(Transition::getType)
.isEqualTo("withChoice");
assertThat(third.getSourceStates()).containsExactly("PAID");
assertThat(third.getTargetStates()).containsExactly("PENDING");
assertThat(third)
.extracting(Transition::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())
.extracting(Transition::getType)
.isEqualTo("withJunction");
assertThat(transitions.get(1))
.extracting(Transition::getType)
.isEqualTo("withJunction");
assertThat(transitions.get(2))
.extracting(Transition::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)
.extracting(Transition::getType)
.isEqualTo("withInternal");
assertThat(transition.getSourceStates()).containsExactly("PAID");
assertThat(transition)
.extracting(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)
.extracting(Transition::getType)
.isEqualTo("withLocal");
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("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)
.extracting(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().getFirst();
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,282 @@
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;
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 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;
}
}

View File

@@ -0,0 +1,293 @@
package click.kamil.springstatemachineexporter.ast.in;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
class AstFileFinderTest {
private static final String CLASS_WITH_ANNOTATION = """
@Service
public class TestService {
public void doSomething() {
// implementation
}
}
""";
private static final String CLASS_WITH_MULTIPLE_ANNOTATIONS = """
@Service
@Component
public class TestService {
public void doSomething() {
// implementation
}
}
""";
private static final String INTERFACE_WITH_ANNOTATION = """
@FunctionalInterface
public interface TestInterface {
void doSomething();
}
""";
private static final String CLASS_WITHOUT_ANNOTATION = """
public class TestService {
public void doSomething() {
// implementation
}
}
""";
private static final String CLASS_WITH_DIFFERENT_ANNOTATION = """
@Repository
public class TestRepository {
public void doSomething() {
// implementation
}
}
""";
private static final String CLASS_WITH_FULLY_QUALIFIED_ANNOTATION = """
@org.springframework.stereotype.Service
public class TestService {
public void doSomething() {
// implementation
}
}
""";
private static final String MULTIPLE_CLASSES = """
@Service
public class TestService {
public void doSomething() {
// implementation
}
}
public class AnotherClass {
// no annotation
}
""";
private static final String CLASS_WITH_ANNOTATION_AND_FIELDS = """
@Service
public class TestService {
private String name;
public void doSomething() {
// implementation
}
}
""";
private static final String EMPTY_SOURCE = "";
private static final String INVALID_JAVA_SYNTAX = """
@Service
public class TestService {
// missing closing brace
""";
@Test
void shouldReturnTrue_whenClassHasMatchingAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnTrue_whenClassHasMultipleMatchingAnnotations() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service", "Component");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_MULTIPLE_ANNOTATIONS, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnFalse_whenClassHasNoAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITHOUT_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenClassHasDifferentAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_DIFFERENT_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnTrue_whenClassHasFullyQualifiedAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("org.springframework.stereotype.Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_FULLY_QUALIFIED_ANNOTATION, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnTrue_whenMultipleClassesAndOneHasAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(MULTIPLE_CLASSES, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnFalse_whenInterfaceHasAnnotation() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("FunctionalInterface");
boolean result = finder.hasClassWithAnnotation(INTERFACE_WITH_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenEmptyAnnotationList() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = Collections.emptyList();
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenEmptySource() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(EMPTY_SOURCE, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldThrowException_whenNullAnnotationList() {
AstFileFinder finder = new AstFileFinder();
// when & then
assertThatThrownBy(() ->
finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, null)
).isInstanceOf(NullPointerException.class);
}
@Test
void shouldReturnTrue_whenClassHasAnnotationAndFields() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION_AND_FIELDS, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldHandleInvalidJavaSyntax() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(INVALID_JAVA_SYNTAX, annotationNames);
// AST parser might still create a valid AST even with syntax errors
// The result depends on how the parser handles the invalid syntax
assertThat(result).isTrue();
}
@Test
void shouldReturnTrue_whenAnnotationNameMatchesCaseInsensitive() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("service"); // lowercase
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isFalse(); // AST parser is case-sensitive
}
@Test
void shouldReturnTrue_whenMultipleAnnotationNamesProvided() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Component", "Service", "Repository");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isTrue();
}
@Test
void shouldReturnFalse_whenNoMatchingAnnotationNames() {
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Component", "Repository", "Controller");
boolean result = finder.hasClassWithAnnotation(CLASS_WITH_ANNOTATION, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenSourceContainsOnlyComments() {
String sourceWithComments = """
// This is a comment
/* Another comment */
/** JavaDoc comment */
""";
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(sourceWithComments, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnFalse_whenSourceContainsOnlyPackageDeclaration() {
String sourceWithPackage = "package com.example;";
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(sourceWithPackage, annotationNames);
assertThat(result).isFalse();
}
@Test
void shouldReturnTrue_whenClassHasAnnotationWithParameters() {
String classWithAnnotatedParams = """
@Service("testService")
public class TestService {
public void doSomething() {
// implementation
}
}
""";
AstFileFinder finder = new AstFileFinder();
List<String> annotationNames = List.of("Service");
boolean result = finder.hasClassWithAnnotation(classWithAnnotatedParams, annotationNames);
assertThat(result).isTrue();
}
}

View File

@@ -0,0 +1,10 @@
package click.kamil.springstatemachineexporter.utils;
import java.nio.file.Path;
public record TestScenario(
String name,
Path inputPath,
Path goldenPath,
String baseName
) {}