Fix HTML export linking by restoring trigger canonicalization and state deduplication.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.enricher;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Event;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
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 java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class EnricherPipelineRegressionTest {
|
||||
|
||||
@Test
|
||||
void shouldCanonicalizeChainTriggerEventsBeforeLinker(@TempDir Path tempDir) throws IOException {
|
||||
writeSampleMachine(tempDir);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TriggerPoint rawTrigger = TriggerPoint.builder()
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("pay")
|
||||
.event("OrderEvent.PAY")
|
||||
.sourceState("OrderState.NEW")
|
||||
.eventTypeFqn("OrderEvent")
|
||||
.stateTypeFqn("OrderState")
|
||||
.build();
|
||||
|
||||
CallChain rawChain = CallChain.builder()
|
||||
.triggerPoint(rawTrigger)
|
||||
.build();
|
||||
|
||||
CodebaseIntelligenceProvider intelligence = new CodebaseIntelligenceProvider() {
|
||||
@Override
|
||||
public List<TriggerPoint> findTriggerPoints() {
|
||||
return List.of(rawTrigger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EntryPoint> findEntryPoints() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CallChain> findCallChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
return List.of(rawChain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Map<String, String>> resolveProperties() {
|
||||
return Map.of();
|
||||
}
|
||||
};
|
||||
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));
|
||||
transition.setSourceStates(List.of(State.of("OrderState.NEW", "com.example.order.OrderState.NEW")));
|
||||
transition.setTargetStates(List.of(State.of("OrderState.PAID", "com.example.order.OrderState.PAID")));
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.transitions(List.of(transition))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.entryPoints(List.of())
|
||||
.triggers(List.of(rawTrigger))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
new CallChainEnricher().enrich(result, context, intelligence);
|
||||
new TriggerCanonicalizationEnricher().enrich(result, context, intelligence);
|
||||
new TransitionLinkerEnricher().enrich(result, context, intelligence);
|
||||
|
||||
TriggerPoint chainTrigger = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||
assertThat(chainTrigger.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
assertThat(chainTrigger.getSourceState()).isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertThat(result.getMetadata().getTriggers().get(0).getEvent())
|
||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
|
||||
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||
}
|
||||
|
||||
private static void writeSampleMachine(Path tempDir) throws IOException {
|
||||
Path orderPkg = tempDir.resolve("com/example/order");
|
||||
Path configPkg = tempDir.resolve("com/example/config");
|
||||
Path webPkg = tempDir.resolve("com/example/web");
|
||||
Files.createDirectories(orderPkg);
|
||||
Files.createDirectories(configPkg);
|
||||
Files.createDirectories(webPkg);
|
||||
|
||||
Files.writeString(orderPkg.resolve("OrderState.java"),
|
||||
"""
|
||||
package com.example.order;
|
||||
public enum OrderState { NEW, PAID }
|
||||
""");
|
||||
Files.writeString(orderPkg.resolve("OrderEvent.java"),
|
||||
"""
|
||||
package com.example.order;
|
||||
public enum OrderEvent { PAY }
|
||||
""");
|
||||
Files.writeString(configPkg.resolve("OrderStateMachineConfiguration.java"),
|
||||
"""
|
||||
package com.example.config;
|
||||
import com.example.order.OrderEvent;
|
||||
import com.example.order.OrderState;
|
||||
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
|
||||
public class OrderStateMachineConfiguration
|
||||
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
||||
}
|
||||
""");
|
||||
Files.writeString(webPkg.resolve("OrderController.java"),
|
||||
"""
|
||||
package com.example.web;
|
||||
public class OrderController {
|
||||
public void pay() {}
|
||||
}
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,46 @@ class MachineEnumCanonicalizerTest {
|
||||
.isEqualTo("getType()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSyncFnFormRawNameWhenFullIdentifierAlreadyCanonical(@TempDir Path tempDir) throws IOException {
|
||||
writeSampleConfig(tempDir);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
||||
"com.example.config.OrderStateMachineConfiguration", context);
|
||||
|
||||
State mismatched = State.of("UNDEFINED", "com.example.order.OrderState.NEW");
|
||||
State canonical = MachineEnumCanonicalizer.canonicalizeStates(Set.of(mismatched), types.stateTypeFqn())
|
||||
.iterator().next();
|
||||
|
||||
assertThat(canonical.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertThat(canonical.rawName()).isEqualTo("OrderState.NEW");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDedupeStatesByCanonicalFullIdentifier(@TempDir Path tempDir) throws IOException {
|
||||
writeSampleConfig(tempDir);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
StateMachineTypeResolver.MachineTypes types = StateMachineTypeResolver.resolveTypes(
|
||||
"com.example.config.OrderStateMachineConfiguration", context);
|
||||
|
||||
State shortForm = State.of("OrderState.NEW", "OrderState.NEW");
|
||||
State packageForm = State.of("UNDEFINED", "com.example.order.OrderState.NEW");
|
||||
|
||||
Set<State> deduped = MachineEnumCanonicalizer.canonicalizeStates(
|
||||
Set.of(shortForm, packageForm), types.stateTypeFqn());
|
||||
|
||||
assertThat(deduped).hasSize(1);
|
||||
State only = deduped.iterator().next();
|
||||
assertThat(only.fullIdentifier()).isEqualTo("com.example.order.OrderState.NEW");
|
||||
assertThat(only.rawName()).isEqualTo("OrderState.NEW");
|
||||
}
|
||||
|
||||
private static void writeSampleConfig(Path tempDir) throws IOException {
|
||||
Path orderPkg = tempDir.resolve("com/example/order");
|
||||
Path configPkg = tempDir.resolve("com/example/config");
|
||||
|
||||
Reference in New Issue
Block a user