645 lines
29 KiB
Java
645 lines
29 KiB
Java
package com.ruoyi.shadow.kernel;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
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.shadow.evidence.ShadowCycleEvidence;
|
|
import com.ruoyi.shadow.evidence.ShadowCycleEvidenceCollector;
|
|
import com.ruoyi.shadow.evidence.ShadowEvidenceDigest;
|
|
import com.ruoyi.shadow.evidence.ShadowEvidenceException;
|
|
import com.ruoyi.shadow.evidence.ShadowEvidenceRepository;
|
|
import com.ruoyi.shadow.evidence.ShadowEvidenceStorageHealth;
|
|
import com.ruoyi.shadow.startup.ShadowStartupReport;
|
|
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.UUID;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.function.Supplier;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
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());
|
|
}
|
|
|
|
@Test
|
|
void completeCyclePersistsEvidenceInsideOuterTransactionBeforeRecording() {
|
|
Instant now = Instant.parse("2026-08-14T06:00:00Z");
|
|
RecordingEvidenceRepository evidence =
|
|
new RecordingEvidenceRepository();
|
|
ShadowStartupReport report = ShadowStartupReport.starting(now,
|
|
Collections.emptyMap());
|
|
RecordingFt546CommandGateway gateway = gateway();
|
|
Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway);
|
|
ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now,
|
|
Collections.singletonList(queueWithoutLegacyAssignment(now)),
|
|
Collections.singletonList(vehicle(now)), maps());
|
|
|
|
ShadowCycleResult result = cycle.run(batch, now);
|
|
|
|
assertEquals("COMPARABLE", result.getStatusCode());
|
|
assertEquals(1, evidence.saved.size());
|
|
assertEquals(1, gateway.getRecorded().size());
|
|
assertTrue(evidence.dispatchObservedInsideTransaction);
|
|
assertEquals(ShadowStartupReport.Status.READY, report.getStatus());
|
|
assertEquals(now, report.getLastCompletedCycle());
|
|
}
|
|
|
|
@Test
|
|
void evidenceCompletionAndReadyTimeIncludeWorkInsteadOfReusingEntryTime() {
|
|
Instant now = Instant.parse("2026-09-06T23:59:59Z");
|
|
java.util.concurrent.atomic.AtomicReference<Instant> time =
|
|
new java.util.concurrent.atomic.AtomicReference<Instant>(now);
|
|
Clock clock = new Clock() {
|
|
public java.time.ZoneId getZone() { return ZoneOffset.UTC; }
|
|
public Clock withZone(java.time.ZoneId zone) { return this; }
|
|
public Instant instant() { return time.get(); }
|
|
};
|
|
RecordingEvidenceRepository evidence = new RecordingEvidenceRepository();
|
|
java.util.concurrent.atomic.AtomicLong nanos = new java.util.concurrent.atomic.AtomicLong();
|
|
ShadowCycleDiagnostics progress = new ShadowCycleDiagnostics(nanos::get);
|
|
evidence.beforeCommit = () -> {
|
|
time.set(now.plusSeconds(3));
|
|
nanos.set(3000000000L);
|
|
};
|
|
ShadowStartupReport report = ShadowStartupReport.starting(now, Collections.emptyMap());
|
|
Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway(), clock,
|
|
() -> {
|
|
time.set(now.plusSeconds(2));
|
|
nanos.set(2000000000L);
|
|
});
|
|
ShadowFactBatch batch = ShadowFactBatch.create(now.minusMillis(50), now,
|
|
Collections.singletonList(queueWithoutLegacyAssignment(now)),
|
|
Collections.singletonList(vehicle(now)), maps());
|
|
assertEquals("COMPARABLE", cycle.run(batch, now, progress).getStatusCode());
|
|
assertEquals(now.plusSeconds(2), evidence.saved.get(0).getCompletedAt());
|
|
assertEquals(now.plusSeconds(3), report.getLastCompletedCycle());
|
|
assertEquals(Long.valueOf(3000), report.getSourceLagMillis());
|
|
Map<?, ?> timings = (Map<?, ?>) progress.finish("COMPARABLE", null).get("phaseMillis");
|
|
assertEquals(1000L, timings.get("TRANSACTION_COMMIT"));
|
|
assertEquals(2000L, timings.get("DISPATCH_AND_STATE_SAVE"));
|
|
}
|
|
|
|
@Test
|
|
void evidenceFailureRecordsNoCommandAndStopsLaterCalculation() {
|
|
Instant now = Instant.parse("2026-08-14T06:00:00Z");
|
|
RecordingEvidenceRepository evidence =
|
|
new RecordingEvidenceRepository();
|
|
evidence.saveFailure = new ShadowEvidenceException(
|
|
"EVIDENCE_WRITE_FAILED");
|
|
ShadowStartupReport report = ShadowStartupReport.starting(now,
|
|
Collections.emptyMap());
|
|
RecordingFt546CommandGateway gateway = gateway();
|
|
Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway);
|
|
ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now,
|
|
Collections.singletonList(queueWithoutLegacyAssignment(now)),
|
|
Collections.singletonList(vehicle(now)), maps());
|
|
|
|
ShadowCycleResult first = cycle.run(batch, now);
|
|
ShadowCycleResult second = cycle.run(batch, now.plusSeconds(1));
|
|
|
|
assertEquals("DEGRADED", first.getStatusCode());
|
|
assertEquals("EVIDENCE_STORAGE_UNAVAILABLE", first.getReasonCode());
|
|
assertEquals("DEGRADED", second.getStatusCode());
|
|
assertEquals(1, evidence.transactionCalls.get());
|
|
assertEquals(Collections.emptyList(), gateway.getRecorded());
|
|
assertEquals(ShadowStartupReport.Status.DEGRADED,
|
|
report.getStatus());
|
|
assertEquals("EVIDENCE_STORAGE_UNAVAILABLE",
|
|
report.getReasonCode());
|
|
}
|
|
|
|
@Test
|
|
void evidenceCollisionKeepsItsDistinctFailClosedReason() {
|
|
Instant now = Instant.parse("2026-08-14T06:00:00Z");
|
|
RecordingEvidenceRepository evidence =
|
|
new RecordingEvidenceRepository();
|
|
evidence.saveFailure = new ShadowEvidenceException(
|
|
"EVIDENCE_KEY_COLLISION");
|
|
ShadowStartupReport report = ShadowStartupReport.starting(now,
|
|
Collections.emptyMap());
|
|
RecordingFt546CommandGateway gateway = gateway();
|
|
Ft546ShadowCycle cycle = evidenceCycle(now, evidence, report, gateway);
|
|
ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now,
|
|
Collections.singletonList(queueWithoutLegacyAssignment(now)),
|
|
Collections.singletonList(vehicle(now)), maps());
|
|
|
|
ShadowCycleResult first = cycle.run(batch, now);
|
|
ShadowCycleResult second = cycle.run(batch, now.plusSeconds(1));
|
|
|
|
assertEquals("DEGRADED", first.getStatusCode());
|
|
assertEquals("EVIDENCE_KEY_COLLISION", first.getReasonCode());
|
|
assertEquals("EVIDENCE_KEY_COLLISION", second.getReasonCode());
|
|
assertEquals("EVIDENCE_KEY_COLLISION", report.getReasonCode());
|
|
assertEquals(1, evidence.transactionCalls.get());
|
|
assertEquals(Collections.emptyList(), gateway.getRecorded());
|
|
}
|
|
|
|
@Test
|
|
void unhealthyStorageDegradesBeforeAssemblyAndIsNotRechecked() {
|
|
Instant now = Instant.parse("2026-08-14T06:00:00Z");
|
|
AtomicInteger healthChecks = new AtomicInteger();
|
|
AtomicInteger assemblyCalls = new AtomicInteger();
|
|
AtomicInteger dispatchCalls = new AtomicInteger();
|
|
ShadowEvidenceStorageHealth health = () -> {
|
|
healthChecks.incrementAndGet();
|
|
return ShadowEvidenceStorageHealth.Check.unavailable(
|
|
"FREE_SPACE_BELOW_MINIMUM");
|
|
};
|
|
RecordingEvidenceRepository evidence =
|
|
new RecordingEvidenceRepository();
|
|
ShadowStartupReport report = ShadowStartupReport.starting(now,
|
|
Collections.emptyMap());
|
|
Ft546ShadowCycle cycle = new Ft546ShadowCycle(
|
|
new ShadowFactBatchValidator(Duration.ofSeconds(4),
|
|
Duration.ofSeconds(30)),
|
|
batch -> {
|
|
assemblyCalls.incrementAndGet();
|
|
throw new AssertionError("must not assemble");
|
|
},
|
|
new ReadOnlyDispatchCycle<Ft546SrgvCommandIntent>(snapshot -> {
|
|
dispatchCalls.incrementAndGet();
|
|
return DispatchCycleResult.retrySnapshotChanged();
|
|
}, new ShadowCommandIntentFactory()::create),
|
|
gateway(), health, evidence,
|
|
collector(evidence.runId, 3L), report);
|
|
ShadowFactBatch batch = ShadowFactBatch.create(now.minusSeconds(1), now,
|
|
Collections.singletonList(queueWithoutLegacyAssignment(now)),
|
|
Collections.singletonList(vehicle(now)), maps());
|
|
|
|
ShadowCycleResult first = cycle.run(batch, now);
|
|
ShadowCycleResult second = cycle.run(batch, now.plusSeconds(1));
|
|
|
|
assertEquals("DEGRADED", first.getStatusCode());
|
|
assertEquals("FREE_SPACE_BELOW_MINIMUM", first.getReasonCode());
|
|
assertEquals("DEGRADED", second.getStatusCode());
|
|
assertEquals(1, healthChecks.get());
|
|
assertEquals(0, assemblyCalls.get());
|
|
assertEquals(0, dispatchCalls.get());
|
|
assertEquals(0, evidence.transactionCalls.get());
|
|
}
|
|
|
|
private static Ft546ShadowCycle evidenceCycle(
|
|
Instant now, RecordingEvidenceRepository evidence,
|
|
ShadowStartupReport report,
|
|
RecordingFt546CommandGateway gateway) {
|
|
return evidenceCycle(now, evidence, report, gateway,
|
|
Clock.fixed(now, ZoneOffset.UTC), () -> { });
|
|
}
|
|
|
|
private static Ft546ShadowCycle evidenceCycle(
|
|
Instant now, RecordingEvidenceRepository evidence,
|
|
ShadowStartupReport report, RecordingFt546CommandGateway gateway,
|
|
Clock clock, Runnable onDispatch) {
|
|
RoutePlanner routePlanner = new AStarRoutePlanner();
|
|
DispatchCoordinator coordinator = new DispatchCoordinator(
|
|
new DefaultVehicleAssignmentPolicy(routePlanner,
|
|
DispatchPolicy.NONE),
|
|
new DeadlockGuard(new WaitForGraph(), routePlanner),
|
|
commit -> {
|
|
evidence.dispatchObservedInsideTransaction =
|
|
evidence.insideTransaction;
|
|
onDispatch.run();
|
|
return true;
|
|
}, () -> "SHADOW-1");
|
|
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));
|
|
return new Ft546ShadowCycle(
|
|
new ShadowFactBatchValidator(Duration.ofSeconds(4),
|
|
Duration.ofSeconds(30)),
|
|
assembler::assemble, readOnly, gateway,
|
|
() -> ShadowEvidenceStorageHealth.Check.available(
|
|
Long.MAX_VALUE),
|
|
evidence, collector(evidence.runId, 3L), report, clock);
|
|
}
|
|
|
|
private static ShadowCycleEvidenceCollector collector(
|
|
UUID runId, long routeVersion) {
|
|
return new ShadowCycleEvidenceCollector(runId, routeVersion,
|
|
new ObjectMapper().findAndRegisterModules());
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
private static final class RecordingEvidenceRepository
|
|
implements ShadowEvidenceRepository {
|
|
private final UUID runId = UUID.randomUUID();
|
|
private final List<ShadowCycleEvidence> saved =
|
|
new java.util.ArrayList<ShadowCycleEvidence>();
|
|
private final AtomicInteger transactionCalls = new AtomicInteger();
|
|
private boolean insideTransaction;
|
|
private boolean dispatchObservedInsideTransaction;
|
|
private RuntimeException saveFailure;
|
|
private Runnable beforeCommit = () -> { };
|
|
|
|
@Override
|
|
public SavedCycle save(ShadowCycleEvidence evidence) {
|
|
return saveAll(Collections.singletonList(evidence)).get(0);
|
|
}
|
|
|
|
@Override
|
|
public List<SavedCycle> saveAll(List<ShadowCycleEvidence> values) {
|
|
assertTrue(insideTransaction,
|
|
"evidence must be saved inside the outer transaction");
|
|
if (saveFailure != null) {
|
|
throw saveFailure;
|
|
}
|
|
List<SavedCycle> result = new java.util.ArrayList<SavedCycle>();
|
|
for (ShadowCycleEvidence value : values) {
|
|
saved.add(value);
|
|
result.add(new SavedCycle(value.getCycleId(),
|
|
value.getEvidenceHash(), value.getStatus(),
|
|
value.getReasonCode()));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public <T> T inTransaction(Supplier<T> work) {
|
|
transactionCalls.incrementAndGet();
|
|
insideTransaction = true;
|
|
try {
|
|
T result = work.get();
|
|
beforeCommit.run();
|
|
return result;
|
|
} finally {
|
|
insideTransaction = false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public SavedCycle findCycle(UUID runId, String inputBatchHash,
|
|
String parentTaskId, String phase,
|
|
long routeVersion) {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public ShadowEvidenceDigest dailyDigest(
|
|
java.time.LocalDate date, java.time.ZoneId zoneId) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public boolean isHealthy() {
|
|
return true;
|
|
}
|
|
}
|
|
}
|