This commit is contained in:
2026-06-19 19:18:23 +02:00
parent d6b1571f18
commit 344e295106
21 changed files with 628 additions and 65 deletions

View File

@@ -23,8 +23,7 @@ import java.util.List;
public class GoldenUpdater {
private static final List<StateMachineExporter> exporters = List.of(new PlantUml(), new Dot(), new Scxml(), new JsonExporter());
private static final EnrichmentService enrichmentService = new EnrichmentService(Collections.emptyList());
private static final ExportService exportService = new ExportService(exporters, enrichmentService);
private static final ExportService exportService = new ExportService(exporters);
public static void main(String[] args) throws Exception {
List<TestScenario> scenarios = provideTestScenarios();

View File

@@ -118,4 +118,50 @@ class TransitionLinkerEnricherTest {
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).isNull();
}
@Test
void shouldMatchWildcardVariable() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("event").build())
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
}
@Test
void shouldMatchMethodCallWildcard() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("richEvent.getType()").build())
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
}
}

View File

@@ -0,0 +1,60 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class PropertyResolverTest {
@Test
void testYamlPropertiesAreLoadedAndFlattened(@TempDir Path tempDir) throws IOException {
Path applicationYml = tempDir.resolve("application.yml");
Files.writeString(applicationYml, """
messaging:
sqs:
integration-system1:
callback:
queue: "integration-system1-queue"
""");
PropertyResolver resolver = new PropertyResolver();
Map<String, String> props = resolver.resolveProperties(tempDir);
assertThat(props).containsEntry("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue");
}
@Test
void testPropertiesFilesAreLoaded(@TempDir Path tempDir) throws IOException {
Path applicationProps = tempDir.resolve("application.properties");
Files.writeString(applicationProps, "messaging.sqs.integration-system1.callback.queue=integration-system1-queue-props");
PropertyResolver resolver = new PropertyResolver();
Map<String, String> props = resolver.resolveProperties(tempDir);
assertThat(props).containsEntry("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue-props");
}
@Test
void testPlaceholderResolution() {
PropertyResolver resolver = new PropertyResolver();
Map<String, String> properties = Map.of("messaging.sqs.integration-system1.callback.queue", "integration-system1-queue");
String resolved = resolver.resolveValue("SQS: ${messaging.sqs.integration-system1.callback.queue}", properties);
assertThat(resolved).isEqualTo("SQS: integration-system1-queue");
}
@Test
void testPlaceholderResolutionWithDefault() {
PropertyResolver resolver = new PropertyResolver();
Map<String, String> properties = Map.of();
String resolved = resolver.resolveValue("SQS: ${messaging.sqs.queue:default-queue}", properties);
assertThat(resolved).isEqualTo("SQS: default-queue");
}
}

View File

@@ -0,0 +1,118 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class CallGraphBuilderTest {
@Test
void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
private final EventDispatcher dispatcher = new EventDispatcher();
public void processOrder() {
dispatcher.dispatch("order", this::sendEvent);
}
public void sendEvent(String event) {
// Dummy trigger
}
}
class EventDispatcher {
public void dispatch(String order, java.util.function.Consumer<String> callback) {
callback.accept("MY_EVENT");
}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderService")
.methodName("processOrder")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("sendEvent")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getMethodChain()).containsExactly(
"com.example.OrderService.processOrder",
"com.example.OrderService.sendEvent"
);
}
@Test
void shouldExtractContextMachineId(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class PersisterService {
private final StateMachinePersister persister = new StateMachinePersister();
public void updateOrderState(String orderId) {
StateMachine sm = persister.restore(null, "machine:" + orderId);
sm.sendEvent("PAY");
}
}
class StateMachinePersister {
public StateMachine restore(Object sm, String contextObj) {
return new StateMachine();
}
}
class StateMachine {
public void sendEvent(String event) {}
}
""";
Files.writeString(tempDir.resolve("PersisterService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.PersisterService")
.methodName("updateOrderState")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("PAY")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getContextMachineId()).isNotNull();
// Since it's tracing constant concatenation: "machine:" + orderId, which might resolve dynamically or string format.
// It should at least be present (it typically extracts 'machine:' + orderId into expression).
}
}

View File

@@ -251,7 +251,7 @@
"lineNumber" : 17
},
"contextMachineId" : null,
"matchedTransitions" : [ ]
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",

View File

@@ -287,7 +287,7 @@
"lineNumber" : 29
},
"contextMachineId" : "orderId",
"matchedTransitions" : [ ]
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",

View File

@@ -287,7 +287,7 @@
"lineNumber" : 29
},
"contextMachineId" : "orderId",
"matchedTransitions" : [ ]
"matchedTransitions" : null
}, {
"entryPoint" : {
"type" : "REST",