feat(ft546): run shared dispatch kernel in shadow

This commit is contained in:
zgw
2026-08-14 15:40:31 +08:00
parent 08003407eb
commit 6864010832
22 changed files with 2346 additions and 8 deletions

View File

@@ -4,6 +4,11 @@ import com.ruoyi.project.shadowtest.ForbiddenFactoryBean;
import com.ruoyi.shadow.startup.ShadowDatabaseGate;
import com.ruoyi.shadow.startup.ShadowStartupReport;
import com.ruoyi.shadow.web.ShadowStatusController;
import com.ruoyi.shadow.kernel.Ft546ShadowCycle;
import com.ruoyi.shadow.kernel.JdbcShadowTrafficStateRepository;
import com.ruoyi.shadow.kernel.RecordingFt546CommandGateway;
import com.ruoyi.shadow.kernel.ShadowDispatchSnapshotAssembler;
import com.ruoyi.wcs.application.ReadOnlyDispatchCycle;
import com.ruoyi.wcs.application.dispatch.DispatchCoordinator;
import com.ruoyi.wcs.port.out.DeviceCommandGateway;
import com.ruoyi.wcs.port.out.WmsNotificationGateway;
@@ -63,6 +68,16 @@ class Ft546ShadowApplicationTest {
assertEquals("PASS", context.getBean(ShadowStartupReport.class)
.getStartupChecks().get("EVIDENCE_DATABASE_VERIFIED"));
assertNotNull(context.getBean(DispatchCoordinator.class));
assertNotNull(context.getBean(ReadOnlyDispatchCycle.class));
assertNotNull(context.getBean(Ft546ShadowCycle.class));
assertNotNull(context.getBean(ShadowDispatchSnapshotAssembler.class));
assertNotNull(context.getBean(RecordingFt546CommandGateway.class));
assertNotNull(context.getBean(
JdbcShadowTrafficStateRepository.class));
assertEquals(context.getBean(
JdbcShadowTrafficStateRepository.class),
context.getBean(com.ruoyi.wcs.port.out
.TrafficStateRepository.class));
assertNotNull(context.getBean(ShadowStatusController.class));
Map<String, String> persisted = context.getBean(
RecordingShadowDatabaseGate.class).getPersistedChecks();

View File

@@ -0,0 +1,362 @@
package com.ruoyi.shadow.kernel;
import com.ruoyi.ft546.protocol.Ft546DeviceProfile;
import com.ruoyi.ft546.protocol.Ft546EncodedCommand;
import com.ruoyi.ft546.protocol.Ft546SrgvCommandEncoder;
import com.ruoyi.ft546.protocol.Ft546SrgvCommandIntent;
import com.ruoyi.shadow.facts.ShadowFactBatch;
import com.ruoyi.shadow.facts.ShadowFactBatchValidator;
import com.ruoyi.shadow.facts.ShadowMapFact;
import com.ruoyi.shadow.facts.ShadowQueueFact;
import com.ruoyi.shadow.facts.ShadowVehicleFact;
import com.ruoyi.wcs.application.ReadOnlyDispatchCycle;
import com.ruoyi.wcs.application.ShadowDispatchResult;
import com.ruoyi.wcs.application.dispatch.DispatchCoordinator;
import com.ruoyi.wcs.application.dispatch.DispatchCycleResult;
import com.ruoyi.wcs.application.dispatch.DispatchSnapshot;
import com.ruoyi.wcs.application.dispatch.GrantedDispatch;
import com.ruoyi.wcs.domain.dispatch.DefaultVehicleAssignmentPolicy;
import com.ruoyi.wcs.domain.traffic.AStarRoutePlanner;
import com.ruoyi.wcs.domain.traffic.DeadlockGuard;
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
import com.ruoyi.wcs.domain.traffic.RoutePlan;
import com.ruoyi.wcs.domain.traffic.RoutePlanner;
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
import com.ruoyi.wcs.port.out.DispatchPolicy;
import org.junit.jupiter.api.Test;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class Ft546ShadowCycleTest {
@Test
void ordinaryGrantUsesTheLegacyMainRegisterSequence() {
Ft546EncodedCommand encoded = encode(grant("VEH-2", points("70511", "70512"),
parameters("1", null, null)));
assertEquals(Arrays.asList("4004", "4000", "4002", "4021"),
addresses(encoded));
assertEquals("VEH-2", encoded.getDeviceId());
assertEquals(points(70511, 70512), encoded.getPathPoints());
}
@Test
void crossFloorGrantAddsOnlyTheLegacyCrossFloorRegisters() {
Ft546EncodedCommand encoded = encode(grant("VEH-3", points("70501", "70502"),
parameters("8", "8", null)));
assertEquals(Arrays.asList("4004", "4000", "4002", "4021",
"4020", "4022"), addresses(encoded));
}
@Test
void twoStageGrantsUseTheMatchingLegacyRegisterSequences() {
Ft546EncodedCommand first = encode(grant("VEH-1",
points("70501", "70503"), parameters("1", null, "1")));
Ft546EncodedCommand second = encode(grant("VEH-1",
points("70503", "70504"), parameters("1", null, "2")));
assertEquals(Arrays.asList("4404", "4422", "4402", "4400"),
addresses(first));
assertEquals(Arrays.asList("4404", "4422", "4423", "4402", "4400"),
addresses(second));
}
@Test
void malformedExplicitPhaseParametersFailBeforeAnythingIsRecorded() {
RecordingFt546CommandGateway gateway = gateway();
ShadowCommandIntentFactory factory = new ShadowCommandIntentFactory();
GrantedDispatch invalid = grant("VEH-1", points("70501", "70502"),
parameters("8", null, null));
assertThrows(IllegalArgumentException.class,
() -> gateway.record(factory.create(invalid)));
assertEquals(Collections.emptyList(), gateway.getRecorded());
}
@Test
void recorderKeepsImmutableEncodedEvidenceWithoutSending() {
RecordingFt546CommandGateway gateway = gateway();
Ft546SrgvCommandIntent intent = new ShadowCommandIntentFactory().create(
grant("VEH-1", points("70501", "70502"),
parameters("1", null, null)));
Ft546EncodedCommand encoded = gateway.record(intent);
assertEquals(Collections.singletonList(encoded), gateway.getRecorded());
assertThrows(UnsupportedOperationException.class,
() -> gateway.getRecorded().clear());
}
@Test
void assemblerUsesOnlyActiveQueueAndIndependentVehicleFacts() {
Instant now = Instant.parse("2026-08-14T06:00:00Z");
ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now,
Arrays.asList(queue("shadow_ro.v_wcs_srgv_queue", "ACTIVE", now),
queue("shadow_ro.v_wcs_srgv_queue_finish", "FINISH", now)),
Collections.singletonList(vehicle(now)), maps());
ShadowDispatchSnapshotAssembler assembler =
new ShadowDispatchSnapshotAssembler("FT546", 3L,
topology -> new ShadowTrafficState(
new ResourceLedger(topology),
new WaitForGraph(), 9L,
Collections.emptyMap()),
Clock.fixed(now, ZoneOffset.UTC));
DispatchSnapshot snapshot = assembler.assemble(batch);
assertEquals(1, snapshot.getReadySteps().size());
assertEquals("ACTIVE", snapshot.getReadySteps().get(0).getStepId());
assertEquals("70501",
snapshot.getReadySteps().get(0).getSourceNodeId());
assertEquals("70502",
snapshot.getReadySteps().get(0).getDestinationNodeId());
assertEquals("30001", snapshot.getReadySteps().get(0)
.getCommandParameters().get("parentTaskId"));
assertEquals(1, snapshot.getVehicleSnapshots().size());
assertEquals("VEH-9", snapshot.getVehicleSnapshots().get(0)
.getVehicleId());
assertEquals(9L, snapshot.getSnapshotVersion());
assertEquals(Arrays.asList("70502"),
snapshot.getTopology().neighbors("70501"));
}
@Test
void invalidFactsProduceNoDispatchIntentOrEncodedEvidence() {
Instant now = Instant.parse("2026-08-14T06:00:00Z");
ShadowFactBatch invalid = ShadowFactBatch.create(now.minusSeconds(1), now,
Collections.singletonList(
queue("shadow_ro.v_wcs_srgv_queue", "ACTIVE", now)),
Collections.emptyList(), maps());
AtomicInteger dispatchCalls = new AtomicInteger();
ReadOnlyDispatchCycle<Ft546SrgvCommandIntent> readOnly =
new ReadOnlyDispatchCycle<Ft546SrgvCommandIntent>(
snapshot -> {
dispatchCalls.incrementAndGet();
return DispatchCycleResult.retrySnapshotChanged();
}, new ShadowCommandIntentFactory()::create);
RecordingFt546CommandGateway gateway = gateway();
Ft546ShadowCycle cycle = new Ft546ShadowCycle(
new ShadowFactBatchValidator(Duration.ofSeconds(4),
Duration.ofSeconds(30)),
batch -> {
throw new AssertionError("invalid facts must not assemble");
}, readOnly, gateway);
ShadowCycleResult result = cycle.run(invalid, now);
assertEquals("INSUFFICIENT_FACTS", result.getStatusCode());
assertEquals("MISSING_VEHICLE_FACTS", result.getReasonCode());
assertEquals(0, dispatchCalls.get());
assertEquals(Collections.emptyList(), result.getIntents());
assertEquals(Collections.emptyList(), result.getEncodedCommands());
assertEquals(Collections.emptyList(), gateway.getRecorded());
}
@Test
void validFactsRunTheRealKernelThroughVirtualRegisterEvidence() {
Instant now = Instant.parse("2026-08-14T06:00:00Z");
ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now,
Collections.singletonList(queueWithoutLegacyAssignment(now)),
Collections.singletonList(vehicle(now)), maps());
AtomicInteger commitCalls = new AtomicInteger();
AtomicInteger generatedIds = new AtomicInteger();
RoutePlanner routePlanner = new AStarRoutePlanner();
DispatchCoordinator coordinator = new DispatchCoordinator(
new DefaultVehicleAssignmentPolicy(routePlanner,
DispatchPolicy.NONE),
new DeadlockGuard(new WaitForGraph(), routePlanner),
commit -> {
commitCalls.incrementAndGet();
return true;
},
() -> "SHADOW-" + generatedIds.incrementAndGet());
ReadOnlyDispatchCycle<Ft546SrgvCommandIntent> readOnly =
new ReadOnlyDispatchCycle<Ft546SrgvCommandIntent>(
coordinator::dispatch,
new ShadowCommandIntentFactory()::create);
ShadowDispatchSnapshotAssembler assembler =
new ShadowDispatchSnapshotAssembler("FT546", 3L,
topology -> new ShadowTrafficState(
new ResourceLedger(topology),
new WaitForGraph(), 0L,
Collections.emptyMap()),
Clock.fixed(now, ZoneOffset.UTC));
RecordingFt546CommandGateway gateway = gateway();
Ft546ShadowCycle cycle = new Ft546ShadowCycle(
new ShadowFactBatchValidator(Duration.ofSeconds(4),
Duration.ofSeconds(30)),
assembler::assemble, readOnly, gateway);
ShadowCycleResult result = cycle.run(batch, now);
assertEquals("COMPARABLE", result.getStatusCode());
assertEquals(1, result.getDecisions().getGranted().size());
assertEquals("VEH-9", result.getDecisions().getGranted().get(0)
.getVehicleId());
assertEquals("ACTIVE", result.getDecisions().getGranted().get(0)
.getStepId());
assertEquals(1, commitCalls.get());
assertEquals(1, result.getIntents().size());
assertEquals(1, result.getEncodedCommands().size());
assertEquals("VEH-9", result.getEncodedCommands().get(0)
.getDeviceId());
assertEquals(Arrays.asList("4004", "4000", "4002", "4021"),
addresses(result.getEncodedCommands().get(0)));
assertEquals(1, gateway.getRecorded().size());
}
private static Ft546EncodedCommand encode(GrantedDispatch grant) {
RecordingFt546CommandGateway gateway = gateway();
Ft546SrgvCommandIntent intent =
new ShadowCommandIntentFactory().create(grant);
return gateway.record(intent);
}
private static RecordingFt546CommandGateway gateway() {
return new RecordingFt546CommandGateway(new Ft546SrgvCommandEncoder(),
profile(), capabilities("POINT_TO_POINT_COMMAND",
"TWO_STAGE_ATOMIC_TASK"));
}
private static GrantedDispatch grant(String vehicleId,
List<String> routeNodes,
Map<String, String> parameters) {
RoutePlan route = new RoutePlan(routeNodes,
resourceKeys(routeNodes), 1, 0, 7L);
return new GrantedDispatch("PARENT-1", "STEP-1", vehicleId, route,
Collections.singletonList("RES-1"), 7L, 11L, parameters);
}
private static Map<String, String> parameters(String taskType,
String crossFloorTarget,
String stage) {
Map<String, String> values = new LinkedHashMap<String, String>();
values.put("parentTaskId", "PARENT-1");
values.put("phase", stage == null ? "LOADED_TO_PLACE"
: "TWO_STAGE_" + stage);
values.put("routeVersion", "3");
values.put("taskNo", "30001");
values.put("taskType", taskType);
if (crossFloorTarget != null) {
values.put("crossFloorTarget", crossFloorTarget);
}
if (stage != null) {
values.put("stage", stage);
}
return values;
}
private static List<String> resourceKeys(List<String> nodes) {
return Arrays.asList("NODE:" + nodes.get(0),
"EDGE:" + nodes.get(0) + ':' + nodes.get(1),
"NODE:" + nodes.get(1));
}
private static Ft546DeviceProfile profile() {
Ft546DeviceProfile profile = new Ft546DeviceProfile();
profile.setDeviceId("SHADOW");
Map<String, String> registers = new LinkedHashMap<String, String>();
registers.put("taskNo", "4000");
registers.put("taskType", "4002");
registers.put("path", "4004");
registers.put("trigger", "4021");
registers.put("crossFloorFlag", "4020");
registers.put("crossFloorTarget", "4022");
registers.put("secondTaskNo", "4400");
registers.put("secondTaskType", "4402");
registers.put("secondPath", "4404");
registers.put("secondStageFlag", "4422");
registers.put("secondComplete", "4423");
profile.setRegisters(registers);
profile.setCapabilities(capabilities("POINT_TO_POINT_COMMAND",
"TWO_STAGE_ATOMIC_TASK"));
return profile;
}
private static Set<String> capabilities(String... values) {
return new LinkedHashSet<String>(Arrays.asList(values));
}
private static List<String> points(String... values) {
return Arrays.asList(values);
}
private static List<Integer> points(Integer... values) {
return Arrays.asList(values);
}
private static List<String> addresses(Ft546EncodedCommand encoded) {
java.util.ArrayList<String> values = new java.util.ArrayList<String>();
encoded.getWrites().forEach(write -> values.add(write.getAddress()));
return values;
}
private static ShadowQueueFact queue(String table, String guid,
Instant createdAt) {
return ShadowQueueFact.builder(table, guid)
.taskNo("30001")
.taskType("2")
.priority(2L)
.cargoPickSite("70501")
.cargoPlaceSite("70502")
.startSite("70501")
.endSite("70502")
.createdAt(createdAt.minusSeconds(10))
.agvId("LEGACY-VEHICLE-MUST-NOT-BE-CANDIDATE")
.pathInfo("99999,99998")
.turnPath("99997")
.default02("1")
.build();
}
private static ShadowQueueFact queueWithoutLegacyAssignment(
Instant createdAt) {
return ShadowQueueFact.builder("shadow_ro.v_wcs_srgv_queue", "ACTIVE")
.taskNo("30001")
.taskType("2")
.priority(2L)
.cargoPickSite("70501")
.cargoPlaceSite("70502")
.startSite("70501")
.endSite("70502")
.createdAt(createdAt.minusSeconds(10))
.default02("1")
.build();
}
private static ShadowVehicleFact vehicle(Instant capturedAt) {
return ShadowVehicleFact.builder("VEH-GUID", "VEH-9")
.agvStatus("1")
.currentSite("70501")
.battery(80)
.workMode(5)
.alarm(0)
.status(1)
.errorCode(0)
.allowSend(1)
.currentFloor(1)
.capturedAt(capturedAt)
.build();
}
private static List<ShadowMapFact> maps() {
return Arrays.asList(
new ShadowMapFact("MAP-1", "70501", 0, 0, 1, 1),
new ShadowMapFact("MAP-2", "70502", 1, 0, 1, 1));
}
}

View File

@@ -0,0 +1,285 @@
package com.ruoyi.shadow.kernel;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.ft546.protocol.Ft546DeviceProfile;
import com.ruoyi.ft546.protocol.Ft546SrgvCommandEncoder;
import com.ruoyi.wcs.application.dispatch.DispatchCommit;
import com.ruoyi.wcs.application.dispatch.GrantedDispatch;
import com.ruoyi.wcs.application.dispatch.HeldDispatch;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import com.ruoyi.wcs.domain.topology.TopologyCompiler;
import com.ruoyi.wcs.domain.topology.TopologyDefinition;
import com.ruoyi.wcs.domain.topology.TopologyEdge;
import com.ruoyi.wcs.domain.topology.TopologyNode;
import com.ruoyi.wcs.domain.traffic.ReservationDecision;
import com.ruoyi.wcs.domain.traffic.ReservationRequest;
import com.ruoyi.wcs.domain.traffic.ReservationState;
import com.ruoyi.wcs.domain.traffic.ResourceLedger;
import com.ruoyi.wcs.domain.traffic.RoutePlan;
import com.ruoyi.wcs.domain.traffic.TrafficDecisionType;
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JdbcShadowTrafficStateRepositoryTest {
private JdbcTemplate jdbc;
private TransactionTemplate transaction;
private Clock clock;
private EffectiveTopology topology;
@BeforeEach
void setUp() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(
"jdbc:h2:mem:" + UUID.randomUUID()
+ ";MODE=MSSQLServer;DB_CLOSE_DELAY=-1", "sa", "");
jdbc = new JdbcTemplate(dataSource);
transaction = new TransactionTemplate(
new DataSourceTransactionManager(dataSource));
clock = Clock.fixed(Instant.parse("2026-08-14T06:00:00Z"),
ZoneOffset.UTC);
jdbc.execute("CREATE TABLE shadow_traffic_state ("
+ "state_key nvarchar(256) PRIMARY KEY,"
+ "snapshot_version bigint NOT NULL,"
+ "state_json nvarchar(max) NOT NULL,"
+ "updated_at timestamp NOT NULL)");
jdbc.execute("CREATE TABLE shadow_hold_history ("
+ "parent_task_id nvarchar(128) NOT NULL,"
+ "vehicle_id nvarchar(64) NOT NULL,"
+ "resource_key nvarchar(256) NOT NULL,"
+ "reason_code nvarchar(512) NOT NULL,"
+ "consecutive_count int NOT NULL,"
+ "updated_at timestamp NOT NULL,"
+ "PRIMARY KEY(parent_task_id,vehicle_id,resource_key,reason_code))");
topology = topology();
}
@Test
void restartReloadPreservesReservationsWaitEdgesHoldsAndFencing() {
JdbcShadowTrafficStateRepository first = repository();
DispatchCommit initial = commit(0L, protectedLedger(), waitGraph(),
held("CAPACITY_CONFLICT"));
assertTrue(first.commit(initial));
JdbcShadowTrafficStateRepository restarted = repository();
ShadowTrafficState loaded = restarted.load(topology);
assertEquals(1L, loaded.getSnapshotVersion());
assertEquals(ReservationState.OCCUPIED,
loaded.getLedger().reservation("RES-1").getState());
assertTrue(loaded.getLedger().reservation("RES-1").isCommandUnknown());
assertTrue(loaded.getLedger().reservation("RES-1")
.isRecoveryUnresolved());
assertEquals(Collections.singleton("VEH-2"),
loaded.getWaitForGraph().holdersWaitedOnBy("VEH-1"));
assertEquals(Collections.singletonList(Integer.valueOf(1)),
Arrays.asList(loaded.getConsecutiveHoldCounts().values()
.toArray(new Integer[0])));
assertTrue(restarted.commit(commit(1L, loaded.getLedger(),
loaded.getWaitForGraph(), held("CAPACITY_CONFLICT"))));
ShadowTrafficState twice = repository().load(topology);
assertEquals(2L, twice.getSnapshotVersion());
assertEquals(Collections.singletonList(Integer.valueOf(2)),
Arrays.asList(twice.getConsecutiveHoldCounts().values()
.toArray(new Integer[0])));
ReservationDecision next = twice.getLedger().reserve(
new ReservationRequest("RES-2", "ORDER-2", "STEP-2",
"VEH-2", Collections.singletonList("NODE:B")));
assertTrue(next.isGranted());
assertEquals(2L, next.getFencingToken());
}
@Test
void staleSnapshotVersionIsRejectedWithoutChangingStoredState() {
JdbcShadowTrafficStateRepository repository = repository();
DispatchCommit initial = commit(0L, protectedLedger(), waitGraph(),
held("CAPACITY_CONFLICT"));
assertTrue(repository.commit(initial));
assertFalse(repository.commit(initial));
assertEquals(1L, repository.load(topology).getSnapshotVersion());
}
@Test
void holdWriteFailureRollsBackTrafficStateUpdate() {
JdbcShadowTrafficStateRepository repository = repository();
assertTrue(repository.commit(commit(0L, protectedLedger(), waitGraph(),
held("CAPACITY_CONFLICT"))));
String tooLong = String.join("", Collections.nCopies(600, "X"));
assertThrows(RuntimeException.class, () -> repository.commit(
commit(1L, repository.load(topology).getLedger(), waitGraph(),
held(tooLong))));
assertEquals(1L, repository.load(topology).getSnapshotVersion());
assertEquals(1, jdbc.queryForObject(
"SELECT consecutive_count FROM shadow_hold_history",
Integer.class).intValue());
}
@Test
void unencodableGrantedRouteNeverAdvancesTrafficState() {
JdbcShadowTrafficStateRepository repository = repository();
ListBuilder route = new ListBuilder();
for (int index = 1; index <= 9; index++) {
route.add(Integer.toString(70000 + index));
}
Map<String, String> parameters = new LinkedHashMap<String, String>();
parameters.put("parentTaskId", "PARENT-1");
parameters.put("phase", "LOADED_TO_PLACE");
parameters.put("routeVersion", "3");
parameters.put("taskNo", "30001");
parameters.put("taskType", "1");
GrantedDispatch invalid = new GrantedDispatch("PARENT-1", "STEP-1",
"VEH-1", new RoutePlan(route.values,
Collections.singletonList("NODE:A"), 8, 0, 7L),
Collections.singletonList("RES-1"), 7L, 1L, parameters);
DispatchCommit commit = new DispatchCommit("FT546", 7L, 0L,
Collections.singletonList(invalid), Collections.emptyList(),
Collections.emptyList(), Collections.emptyList(),
new ResourceLedger(topology), new WaitForGraph());
assertThrows(IllegalArgumentException.class,
() -> repository.commit(commit));
assertEquals(0L, repository.load(topology).getSnapshotVersion());
}
@Test
void protocolEncoderRejectionNeverAdvancesTrafficState() {
ShadowCommandIntentFactory intentFactory =
new ShadowCommandIntentFactory();
Ft546DeviceProfile missingPathRegister = new Ft546DeviceProfile();
missingPathRegister.setRegisters(Collections.emptyMap());
RecordingFt546CommandGateway rejectingGateway =
new RecordingFt546CommandGateway(
new Ft546SrgvCommandEncoder(), missingPathRegister,
Collections.singleton("POINT_TO_POINT_COMMAND"));
JdbcShadowTrafficStateRepository repository =
new JdbcShadowTrafficStateRepository(jdbc, transaction,
new ObjectMapper(), clock, intentFactory,
rejectingGateway);
Map<String, String> parameters = new LinkedHashMap<String, String>();
parameters.put("parentTaskId", "PARENT-1");
parameters.put("phase", "LOADED_TO_PLACE");
parameters.put("routeVersion", "3");
parameters.put("taskNo", "30001");
parameters.put("taskType", "1");
GrantedDispatch grant = new GrantedDispatch("PARENT-1", "STEP-1",
"VEH-1", new RoutePlan(Arrays.asList("70001", "70002"),
Collections.singletonList("NODE:A"), 1, 0, 7L),
Collections.singletonList("RES-1"), 7L, 1L, parameters);
DispatchCommit commit = new DispatchCommit("FT546", 7L, 0L,
Collections.singletonList(grant), Collections.emptyList(),
Collections.emptyList(), Collections.emptyList(),
new ResourceLedger(topology), new WaitForGraph());
assertThrows(IllegalArgumentException.class,
() -> repository.commit(commit));
assertEquals(0L, repository.load(topology).getSnapshotVersion());
assertEquals(Collections.emptyList(), rejectingGateway.getRecorded());
}
private JdbcShadowTrafficStateRepository repository() {
ShadowCommandIntentFactory intentFactory =
new ShadowCommandIntentFactory();
return new JdbcShadowTrafficStateRepository(jdbc, transaction,
new ObjectMapper(), clock, intentFactory,
commandGateway());
}
private static RecordingFt546CommandGateway commandGateway() {
Ft546DeviceProfile profile = new Ft546DeviceProfile();
Map<String, String> registers = new LinkedHashMap<String, String>();
registers.put("taskNo", "4000");
registers.put("taskType", "4002");
registers.put("path", "4004");
registers.put("trigger", "4021");
registers.put("crossFloorFlag", "4020");
registers.put("crossFloorTarget", "4022");
registers.put("secondTaskNo", "4400");
registers.put("secondTaskType", "4402");
registers.put("secondPath", "4404");
registers.put("secondStageFlag", "4422");
registers.put("secondComplete", "4423");
profile.setRegisters(registers);
return new RecordingFt546CommandGateway(
new Ft546SrgvCommandEncoder(), profile,
new LinkedHashSet<String>(Arrays.asList(
"POINT_TO_POINT_COMMAND",
"TWO_STAGE_ATOMIC_TASK")));
}
private DispatchCommit commit(long snapshotVersion, ResourceLedger ledger,
WaitForGraph graph, HeldDispatch held) {
return new DispatchCommit("FT546", topology.version(), snapshotVersion,
Collections.emptyList(), Collections.singletonList(held),
Collections.emptyList(), Collections.emptyList(), ledger, graph);
}
private ResourceLedger protectedLedger() {
ResourceLedger ledger = new ResourceLedger(topology);
ReservationDecision reserved = ledger.reserve(new ReservationRequest(
"RES-1", "ORDER-1", "STEP-1", "VEH-1",
Collections.singletonList("NODE:A")));
ledger.occupy("RES-1", reserved.getFencingToken());
ledger.markCommandUnknown("RES-1");
ledger.markRecoveryUnresolved("RES-1");
return ledger;
}
private static WaitForGraph waitGraph() {
WaitForGraph graph = new WaitForGraph();
graph.addWait("VEH-1", "VEH-2");
return graph;
}
private static HeldDispatch held(String reason) {
return new HeldDispatch("PARENT-1", "STEP-1", "VEH-1",
TrafficDecisionType.HOLD, reason,
new LinkedHashSet<String>(Collections.singleton("VEH-2")),
null, null);
}
private static EffectiveTopology topology() {
return new TopologyCompiler().compile(new TopologyDefinition("FT546",
7L, true,
Arrays.asList(
new TopologyNode("A", "F1", 0, 0, 1, false, false),
new TopologyNode("B", "F1", 1, 0, 1, false, false)),
Collections.singletonList(new TopologyEdge("A", "B", 1, 1)),
Collections.emptyList(), Collections.emptyList(),
Collections.emptyList()));
}
private static final class ListBuilder {
private final java.util.List<String> values =
new java.util.ArrayList<String>();
private void add(String value) {
values.add(value);
}
}
}