Expand entry-point and trigger detector coverage.

Detect fire/trigger wrappers as state-machine triggers, resolve Kafka topicPattern and additional Rabbit listener attributes, and add regression tests for WebFlux RouterFunction routes and messaging listeners.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 05:50:34 +02:00
parent b158179559
commit 23a14d8391
6 changed files with 217 additions and 9 deletions

View File

@@ -0,0 +1,46 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class GenericEventDetectorFireTest {
@Test
void shouldDetectFireMethodAsTrigger(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
enum OrderEvent { PAY, SHIP }
class OrderService {
private final StateMachineFacade facade;
OrderService(StateMachineFacade facade) { this.facade = facade; }
public void pay() {
facade.fire(OrderEvent.PAY);
}
}
class StateMachineFacade {
public void fire(OrderEvent event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, context.getConstantResolver(), List.of());
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.OrderService");
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(triggers).hasSize(1);
assertThat(triggers.get(0).getMethodName()).isEqualTo("pay");
assertThat(triggers.get(0).getEvent()).contains("PAY");
}
}

View File

@@ -0,0 +1,78 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class MessagingDetectorTest {
@Test
void shouldDetectRabbitListenerQueue(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class ReturnsListener {
@RabbitListener(queues = "returns.queue")
public void onReturn(String orderId) {}
}
""";
Files.writeString(tempDir.resolve("ReturnsListener.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
MessagingDetector detector = new MessagingDetector(context);
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.ReturnsListener");
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(entryPoints).hasSize(1);
assertThat(entryPoints.get(0).getType()).isEqualTo(EntryPoint.Type.RABBIT);
assertThat(entryPoints.get(0).getName()).isEqualTo("RABBIT: returns.queue");
assertThat(entryPoints.get(0).getMetadata()).containsEntry("destination", "returns.queue");
}
@Test
void shouldDetectKafkaListenerTopicsAndTopicPattern(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class OrderKafkaListener {
@KafkaListener(topics = {"orders.created", "orders.updated"})
public void onOrderEvent(String payload) {}
@KafkaListener(topicPattern = "payments.*")
public void onPaymentEvent(String payload) {}
}
""";
Files.writeString(tempDir.resolve("OrderKafkaListener.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
MessagingDetector detector = new MessagingDetector(context);
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.OrderKafkaListener");
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(entryPoints).hasSize(2);
assertThat(entryPoints).anyMatch(ep ->
ep.getType() == EntryPoint.Type.KAFKA
&& ep.getName().contains("orders.created")
&& ep.getName().contains("orders.updated"));
assertThat(entryPoints).anyMatch(ep ->
ep.getType() == EntryPoint.Type.KAFKA
&& "payments.*".equals(ep.getMetadata().get("destination")));
}
}

View File

@@ -0,0 +1,74 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SpringMvcDetectorTest {
@Test
void shouldDetectWebFluxRouterFunctionBean(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class ReactiveOrderApi {
@Bean
public RouterFunction<ServerResponse> orderRoutes() {
return route().POST("/reactive/order", request -> ServerResponse.ok().build()).build();
}
}
""";
Files.writeString(tempDir.resolve("ReactiveOrderApi.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
SpringMvcDetector detector = new SpringMvcDetector(context, context.getConstantResolver());
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.ReactiveOrderApi");
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(entryPoints).anyMatch(ep ->
"POST /reactive/order".equals(ep.getName())
&& "orderRoutes".equals(ep.getMethodName())
&& "WebFlux Functional".equals(ep.getMetadata().get("style")));
}
@Test
void shouldDetectRestControllerPostMapping(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping("/pay")
public void pay() {}
}
""";
Files.writeString(tempDir.resolve("OrderController.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
SpringMvcDetector detector = new SpringMvcDetector(context, context.getConstantResolver());
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.OrderController");
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(entryPoints).anyMatch(ep ->
"POST /api/orders/pay".equals(ep.getName()) && "pay".equals(ep.getMethodName()));
}
}