feat: add legacy dispatch cycle demo
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ft546.wcs.demo.center;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class CycleResult {
|
||||||
|
private final int cycleNumber;
|
||||||
|
private final List<String> processedVehicleIds;
|
||||||
|
private final List<String> events;
|
||||||
|
|
||||||
|
public CycleResult(
|
||||||
|
int cycleNumber,
|
||||||
|
List<String> processedVehicleIds,
|
||||||
|
List<String> events
|
||||||
|
) {
|
||||||
|
this.cycleNumber = cycleNumber;
|
||||||
|
this.processedVehicleIds = Collections.unmodifiableList(
|
||||||
|
new ArrayList<String>(processedVehicleIds)
|
||||||
|
);
|
||||||
|
this.events = Collections.unmodifiableList(new ArrayList<String>(events));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCycleNumber() {
|
||||||
|
return cycleNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getProcessedVehicleIds() {
|
||||||
|
return processedVehicleIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getEvents() {
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package com.ft546.wcs.demo.center;
|
||||||
|
|
||||||
|
import com.ft546.wcs.demo.model.DecisionCode;
|
||||||
|
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||||
|
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||||
|
import com.ft546.wcs.demo.repository.InMemoryWcsState;
|
||||||
|
import com.ft546.wcs.demo.simulation.SimulatedProtocolAdapter;
|
||||||
|
import com.ft546.wcs.demo.traffic.TrafficController;
|
||||||
|
import com.ft546.wcs.demo.traffic.TrafficDecision;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class LegacyDispatchCenter {
|
||||||
|
private final InMemoryWcsState state;
|
||||||
|
private final QueueAdmissionService admission;
|
||||||
|
private final TrafficController traffic;
|
||||||
|
private final SimulatedProtocolAdapter protocol;
|
||||||
|
|
||||||
|
public LegacyDispatchCenter(
|
||||||
|
InMemoryWcsState state,
|
||||||
|
QueueAdmissionService admission,
|
||||||
|
TrafficController traffic,
|
||||||
|
SimulatedProtocolAdapter protocol
|
||||||
|
) {
|
||||||
|
if (state == null || admission == null || traffic == null || protocol == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"state, admission, traffic and protocol are required"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.state = state;
|
||||||
|
this.admission = admission;
|
||||||
|
this.traffic = traffic;
|
||||||
|
this.protocol = protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CycleResult runCycle() {
|
||||||
|
int eventStart = state.getEvents().size();
|
||||||
|
int cycleNumber = state.nextCycle();
|
||||||
|
state.addEvent("CYCLE " + cycleNumber);
|
||||||
|
|
||||||
|
admission.createPendingQueue(state);
|
||||||
|
|
||||||
|
List<VehicleSnapshot> vehicles =
|
||||||
|
new ArrayList<VehicleSnapshot>(state.getVehicles());
|
||||||
|
Collections.sort(vehicles, new Comparator<VehicleSnapshot>() {
|
||||||
|
@Override
|
||||||
|
public int compare(VehicleSnapshot left, VehicleSnapshot right) {
|
||||||
|
return left.getVehicleId().compareTo(right.getVehicleId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> processed = new ArrayList<String>();
|
||||||
|
for (VehicleSnapshot frozenVehicle : vehicles) {
|
||||||
|
processed.add(frozenVehicle.getVehicleId());
|
||||||
|
Optional<DispatchQueueEntry> optional =
|
||||||
|
state.findQueueByVehicle(frozenVehicle.getVehicleId());
|
||||||
|
if (!optional.isPresent()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DispatchQueueEntry queue = optional.get();
|
||||||
|
if (queue.isIssued()) {
|
||||||
|
processIssuedQueue(frozenVehicle, queue);
|
||||||
|
} else {
|
||||||
|
processUnissuedQueue(frozenVehicle, queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> cycleEvents = new ArrayList<String>(
|
||||||
|
state.getEvents().subList(eventStart, state.getEvents().size())
|
||||||
|
);
|
||||||
|
return new CycleResult(cycleNumber, processed, cycleEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processUnissuedQueue(
|
||||||
|
VehicleSnapshot vehicle,
|
||||||
|
DispatchQueueEntry queue
|
||||||
|
) {
|
||||||
|
TrafficDecision decision = traffic.plan(
|
||||||
|
vehicle,
|
||||||
|
queue,
|
||||||
|
state.getVehicles(),
|
||||||
|
state.getActiveQueues()
|
||||||
|
);
|
||||||
|
if (decision.getCode() == DecisionCode.AVOIDANCE_CREATED) {
|
||||||
|
DispatchQueueEntry avoidance = decision.getAvoidanceQueue().get();
|
||||||
|
state.addQueue(avoidance);
|
||||||
|
state.addEvent(
|
||||||
|
"AVOID queue=" + avoidance.getQueueId() + " reason="
|
||||||
|
+ decision.getReason()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (decision.getCode() != DecisionCode.PATH_READY) {
|
||||||
|
state.addEvent(
|
||||||
|
"HOLD " + decision.getCode() + " queue=" + queue.getQueueId()
|
||||||
|
+ " reason=" + decision.getReason()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.addEvent("PATH queue=" + queue.getQueueId() + " " + decision.getPath());
|
||||||
|
SimulatedProtocolAdapter.ProtocolResult result =
|
||||||
|
protocol.send(queue, decision.getPath());
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
state.addEvent(
|
||||||
|
"HOLD " + DecisionCode.HOLD_PROTOCOL_FAILURE + " queue="
|
||||||
|
+ queue.getQueueId() + " reason=" + result.getMessage()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue.markIssued(decision.getPath());
|
||||||
|
state.addEvent("SEND queue=" + queue.getQueueId() + " vehicle="
|
||||||
|
+ queue.getVehicleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processIssuedQueue(
|
||||||
|
VehicleSnapshot vehicle,
|
||||||
|
DispatchQueueEntry queue
|
||||||
|
) {
|
||||||
|
if (vehicle.getCurrentSite().equals(queue.getEndSite())) {
|
||||||
|
complete(queue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int index = queue.getPath().indexOf(vehicle.getCurrentSite());
|
||||||
|
if (index < 0 || index + 1 >= queue.getPath().size()) {
|
||||||
|
state.addEvent(
|
||||||
|
"HOLD " + DecisionCode.HOLD_NO_PATH + " queue=" + queue.getQueueId()
|
||||||
|
+ " reason=vehicle is outside issued path"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String nextSite = queue.getPath().get(index + 1);
|
||||||
|
state.updateVehicle(vehicle.at(nextSite, vehicle.getFloor()));
|
||||||
|
state.addEvent(
|
||||||
|
"MOVE vehicle=" + vehicle.getVehicleId() + " site=" + nextSite
|
||||||
|
);
|
||||||
|
if (nextSite.equals(queue.getEndSite())) {
|
||||||
|
complete(queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void complete(DispatchQueueEntry queue) {
|
||||||
|
if (state.completeQueue(queue.getQueueId())) {
|
||||||
|
state.addEvent(
|
||||||
|
"COMPLETE queue=" + queue.getQueueId() + " vehicle="
|
||||||
|
+ queue.getVehicleId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.ft546.wcs.demo.center;
|
||||||
|
|
||||||
|
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||||
|
import com.ft546.wcs.demo.model.DispatchTask;
|
||||||
|
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||||
|
import com.ft546.wcs.demo.repository.InMemoryWcsState;
|
||||||
|
import com.ft546.wcs.demo.traffic.GridMap;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class QueueAdmissionService {
|
||||||
|
private final VehicleAssignmentService assignmentService;
|
||||||
|
private final GridMap map;
|
||||||
|
private int invocationCount;
|
||||||
|
|
||||||
|
public QueueAdmissionService(
|
||||||
|
VehicleAssignmentService assignmentService,
|
||||||
|
GridMap map
|
||||||
|
) {
|
||||||
|
if (assignmentService == null || map == null) {
|
||||||
|
throw new IllegalArgumentException("assignment service and map are required");
|
||||||
|
}
|
||||||
|
this.assignmentService = assignmentService;
|
||||||
|
this.map = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<DispatchQueueEntry> createPendingQueue(InMemoryWcsState state) {
|
||||||
|
if (state == null) {
|
||||||
|
throw new IllegalArgumentException("state must not be null");
|
||||||
|
}
|
||||||
|
invocationCount++;
|
||||||
|
if (state.getPendingTasks().isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchTask task = state.getPendingTasks().get(0);
|
||||||
|
List<VehicleSnapshot> available = new ArrayList<VehicleSnapshot>();
|
||||||
|
for (VehicleSnapshot vehicle : state.getVehicles()) {
|
||||||
|
if (!state.findQueueByVehicle(vehicle.getVehicleId()).isPresent()) {
|
||||||
|
available.add(vehicle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VehicleAssignmentDecision decision =
|
||||||
|
assignmentService.select(task, available, map.getSites());
|
||||||
|
if (!decision.getVehicle().isPresent()) {
|
||||||
|
state.addEvent(
|
||||||
|
"HOLD " + decision.getCode() + " task=" + task.getTaskId()
|
||||||
|
+ " reason=" + decision.getReason()
|
||||||
|
);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
VehicleSnapshot selected = decision.getVehicle().get();
|
||||||
|
DispatchQueueEntry queue = DispatchQueueEntry.business(
|
||||||
|
task,
|
||||||
|
selected.getVehicleId()
|
||||||
|
);
|
||||||
|
state.admitTask(task, queue);
|
||||||
|
state.addEvent(
|
||||||
|
"ASSIGN task=" + task.getTaskId() + " vehicle="
|
||||||
|
+ selected.getVehicleId() + " reason=" + decision.getReason()
|
||||||
|
);
|
||||||
|
return Optional.of(queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInvocationCount() {
|
||||||
|
return invocationCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.ft546.wcs.demo.repository;
|
||||||
|
|
||||||
|
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||||
|
import com.ft546.wcs.demo.model.DispatchTask;
|
||||||
|
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class InMemoryWcsState {
|
||||||
|
private final Map<String, VehicleSnapshot> vehicles =
|
||||||
|
new LinkedHashMap<String, VehicleSnapshot>();
|
||||||
|
private final Map<String, DispatchTask> pendingTasks =
|
||||||
|
new LinkedHashMap<String, DispatchTask>();
|
||||||
|
private final Set<String> knownTaskIds = new LinkedHashSet<String>();
|
||||||
|
private final Map<String, DispatchQueueEntry> activeQueues =
|
||||||
|
new LinkedHashMap<String, DispatchQueueEntry>();
|
||||||
|
private final List<DispatchQueueEntry> history =
|
||||||
|
new ArrayList<DispatchQueueEntry>();
|
||||||
|
private final List<String> events = new ArrayList<String>();
|
||||||
|
private int cycleNumber;
|
||||||
|
|
||||||
|
public void addVehicle(VehicleSnapshot vehicle) {
|
||||||
|
if (vehicle == null) {
|
||||||
|
throw new IllegalArgumentException("vehicle must not be null");
|
||||||
|
}
|
||||||
|
if (vehicles.containsKey(vehicle.getVehicleId())) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"duplicate vehicle: " + vehicle.getVehicleId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
vehicles.put(vehicle.getVehicleId(), vehicle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateVehicle(VehicleSnapshot vehicle) {
|
||||||
|
if (vehicle == null || !vehicles.containsKey(vehicle.getVehicleId())) {
|
||||||
|
throw new IllegalArgumentException("cannot update unknown vehicle");
|
||||||
|
}
|
||||||
|
vehicles.put(vehicle.getVehicleId(), vehicle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTask(DispatchTask task) {
|
||||||
|
if (task == null) {
|
||||||
|
throw new IllegalArgumentException("task must not be null");
|
||||||
|
}
|
||||||
|
if (!knownTaskIds.add(task.getTaskId())) {
|
||||||
|
throw new IllegalArgumentException("duplicate task: " + task.getTaskId());
|
||||||
|
}
|
||||||
|
pendingTasks.put(task.getTaskId(), task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void admitTask(DispatchTask task, DispatchQueueEntry queue) {
|
||||||
|
if (task == null || !pendingTasks.containsKey(task.getTaskId())) {
|
||||||
|
throw new IllegalArgumentException("task is not pending: " + task);
|
||||||
|
}
|
||||||
|
addQueue(queue);
|
||||||
|
pendingTasks.remove(task.getTaskId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addQueue(DispatchQueueEntry queue) {
|
||||||
|
if (queue == null) {
|
||||||
|
throw new IllegalArgumentException("queue must not be null");
|
||||||
|
}
|
||||||
|
if (activeQueues.containsKey(queue.getQueueId())) {
|
||||||
|
throw new IllegalArgumentException("duplicate queue: " + queue.getQueueId());
|
||||||
|
}
|
||||||
|
if (findQueueByVehicle(queue.getVehicleId()).isPresent()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"vehicle already has an active queue: " + queue.getVehicleId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
activeQueues.put(queue.getQueueId(), queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean completeQueue(String queueId) {
|
||||||
|
DispatchQueueEntry queue = activeQueues.remove(queueId);
|
||||||
|
if (queue == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
queue.markCompleted();
|
||||||
|
history.add(queue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<VehicleSnapshot> getVehicles() {
|
||||||
|
return Collections.unmodifiableList(
|
||||||
|
new ArrayList<VehicleSnapshot>(vehicles.values())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DispatchTask> getPendingTasks() {
|
||||||
|
List<DispatchTask> result =
|
||||||
|
new ArrayList<DispatchTask>(pendingTasks.values());
|
||||||
|
Collections.sort(result, new Comparator<DispatchTask>() {
|
||||||
|
@Override
|
||||||
|
public int compare(DispatchTask left, DispatchTask right) {
|
||||||
|
int priority = Integer.compare(right.getPriority(), left.getPriority());
|
||||||
|
if (priority != 0) {
|
||||||
|
return priority;
|
||||||
|
}
|
||||||
|
return left.getTaskId().compareTo(right.getTaskId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Collections.unmodifiableList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DispatchQueueEntry> getActiveQueues() {
|
||||||
|
return Collections.unmodifiableList(
|
||||||
|
new ArrayList<DispatchQueueEntry>(activeQueues.values())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DispatchQueueEntry> getHistory() {
|
||||||
|
return Collections.unmodifiableList(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<DispatchQueueEntry> findQueueByVehicle(String vehicleId) {
|
||||||
|
for (DispatchQueueEntry queue : activeQueues.values()) {
|
||||||
|
if (queue.getVehicleId().equals(vehicleId)) {
|
||||||
|
return Optional.of(queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addEvent(String event) {
|
||||||
|
if (event == null || event.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("event must not be blank");
|
||||||
|
}
|
||||||
|
events.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getEvents() {
|
||||||
|
return Collections.unmodifiableList(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int nextCycle() {
|
||||||
|
cycleNumber++;
|
||||||
|
return cycleNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCycleNumber() {
|
||||||
|
return cycleNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.ft546.wcs.demo.simulation;
|
||||||
|
|
||||||
|
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class SimulatedProtocolAdapter {
|
||||||
|
private final List<String> attemptedQueueIds = new ArrayList<String>();
|
||||||
|
private String nextFailure;
|
||||||
|
|
||||||
|
public ProtocolResult send(DispatchQueueEntry queue, List<String> path) {
|
||||||
|
if (queue == null || path == null || path.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("queue and non-empty path are required");
|
||||||
|
}
|
||||||
|
attemptedQueueIds.add(queue.getQueueId());
|
||||||
|
if (nextFailure != null) {
|
||||||
|
String failure = nextFailure;
|
||||||
|
nextFailure = null;
|
||||||
|
return ProtocolResult.failure(failure);
|
||||||
|
}
|
||||||
|
return ProtocolResult.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void failNextSend(String reason) {
|
||||||
|
if (reason == null || reason.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("failure reason must not be blank");
|
||||||
|
}
|
||||||
|
nextFailure = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAttemptedQueueIds() {
|
||||||
|
return Collections.unmodifiableList(attemptedQueueIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ProtocolResult {
|
||||||
|
private final boolean success;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
private ProtocolResult(boolean success, String message) {
|
||||||
|
this.success = success;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProtocolResult success() {
|
||||||
|
return new ProtocolResult(true, "accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProtocolResult failure(String message) {
|
||||||
|
return new ProtocolResult(false, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package com.ft546.wcs.demo;
|
||||||
|
|
||||||
|
import com.ft546.wcs.demo.center.CycleResult;
|
||||||
|
import com.ft546.wcs.demo.center.LegacyDispatchCenter;
|
||||||
|
import com.ft546.wcs.demo.center.QueueAdmissionService;
|
||||||
|
import com.ft546.wcs.demo.center.VehicleAssignmentService;
|
||||||
|
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||||
|
import com.ft546.wcs.demo.model.DispatchTask;
|
||||||
|
import com.ft546.wcs.demo.model.Site;
|
||||||
|
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||||
|
import com.ft546.wcs.demo.repository.InMemoryWcsState;
|
||||||
|
import com.ft546.wcs.demo.simulation.SimulatedProtocolAdapter;
|
||||||
|
import com.ft546.wcs.demo.traffic.GridMap;
|
||||||
|
import com.ft546.wcs.demo.traffic.GridPathPlanner;
|
||||||
|
import com.ft546.wcs.demo.traffic.TrafficController;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class LegacyDispatchCenterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void eachCycleAdmitsOnceAndProcessesVehiclesInStableOrder() {
|
||||||
|
Fixture fixture = fixtureWithThreeVehicles();
|
||||||
|
|
||||||
|
CycleResult result = fixture.center.runCycle();
|
||||||
|
|
||||||
|
assertEquals(1, fixture.admission.getInvocationCount());
|
||||||
|
assertEquals(Arrays.asList("1", "2", "3"), result.getProcessedVehicleIds());
|
||||||
|
assertEquals(1, fixture.state.getActiveQueues().size());
|
||||||
|
assertTrue(fixture.state.getActiveQueues().get(0).isIssued());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void failedProtocolSendRemainsPendingAndRetriesNextCycle() {
|
||||||
|
Fixture fixture = fixtureWithOneVehicle();
|
||||||
|
fixture.protocol.failNextSend("simulated PLC rejection");
|
||||||
|
|
||||||
|
fixture.center.runCycle();
|
||||||
|
|
||||||
|
DispatchQueueEntry pending = fixture.state.getActiveQueues().get(0);
|
||||||
|
assertEquals(0, pending.getIssued());
|
||||||
|
assertEquals(1, pending.getExecutionStatus());
|
||||||
|
assertTrue(fixture.state.getEvents().toString().contains("HOLD_PROTOCOL_FAILURE"));
|
||||||
|
|
||||||
|
fixture.center.runCycle();
|
||||||
|
|
||||||
|
assertEquals(1, pending.getIssued());
|
||||||
|
assertEquals(3, pending.getExecutionStatus());
|
||||||
|
assertEquals(2, fixture.protocol.getAttemptedQueueIds().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void vehicleAtDestinationArchivesAndDeletesQueueExactlyOnce() {
|
||||||
|
GridMap map = simpleMap();
|
||||||
|
InMemoryWcsState state = new InMemoryWcsState();
|
||||||
|
state.addVehicle(vehicle("1", "TARGET"));
|
||||||
|
DispatchQueueEntry queue = DispatchQueueEntry.business(task(), "1");
|
||||||
|
queue.markIssued(Arrays.asList("A", "TARGET"));
|
||||||
|
state.addQueue(queue);
|
||||||
|
Fixture fixture = fixture(map, state);
|
||||||
|
|
||||||
|
fixture.center.runCycle();
|
||||||
|
fixture.center.runCycle();
|
||||||
|
|
||||||
|
assertTrue(fixture.state.getActiveQueues().isEmpty());
|
||||||
|
assertEquals(1, fixture.state.getHistory().size());
|
||||||
|
assertEquals(4, fixture.state.getHistory().get(0).getExecutionStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void channelConflictAddsAvoidanceQueueForBlockingVehicle() {
|
||||||
|
GridMap map = simpleMap();
|
||||||
|
map.setCacheSite("TARGET-CHANNEL", "CACHE");
|
||||||
|
InMemoryWcsState state = new InMemoryWcsState();
|
||||||
|
state.addVehicle(vehicle("1", "A"));
|
||||||
|
state.addVehicle(vehicle("2", "BLOCKER"));
|
||||||
|
state.addTask(task());
|
||||||
|
Fixture fixture = fixture(map, state);
|
||||||
|
|
||||||
|
fixture.center.runCycle();
|
||||||
|
|
||||||
|
assertEquals(2, fixture.state.getActiveQueues().size());
|
||||||
|
assertTrue(fixture.state.findQueueByVehicle("2").isPresent());
|
||||||
|
assertEquals(
|
||||||
|
DispatchQueueEntry.QueueType.AVOIDANCE,
|
||||||
|
fixture.state.findQueueByVehicle("2").get().getQueueType()
|
||||||
|
);
|
||||||
|
assertTrue(fixture.state.getEvents().toString().contains("AVOID"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Fixture fixtureWithThreeVehicles() {
|
||||||
|
GridMap map = simpleMap();
|
||||||
|
InMemoryWcsState state = new InMemoryWcsState();
|
||||||
|
state.addVehicle(vehicle("3", "V3"));
|
||||||
|
state.addVehicle(vehicle("2", "V2"));
|
||||||
|
state.addVehicle(vehicle("1", "A"));
|
||||||
|
state.addTask(task());
|
||||||
|
return fixture(map, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Fixture fixtureWithOneVehicle() {
|
||||||
|
GridMap map = simpleMap();
|
||||||
|
InMemoryWcsState state = new InMemoryWcsState();
|
||||||
|
state.addVehicle(vehicle("1", "A"));
|
||||||
|
state.addTask(task());
|
||||||
|
return fixture(map, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Fixture fixture(GridMap map, InMemoryWcsState state) {
|
||||||
|
QueueAdmissionService admission = new QueueAdmissionService(
|
||||||
|
new VehicleAssignmentService(),
|
||||||
|
map
|
||||||
|
);
|
||||||
|
SimulatedProtocolAdapter protocol = new SimulatedProtocolAdapter();
|
||||||
|
LegacyDispatchCenter center = new LegacyDispatchCenter(
|
||||||
|
state,
|
||||||
|
admission,
|
||||||
|
new TrafficController(map, new GridPathPlanner()),
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
return new Fixture(state, admission, protocol, center);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GridMap simpleMap() {
|
||||||
|
GridMap map = new GridMap();
|
||||||
|
map.addSite(new Site("A", 1, 0, 0, "START"));
|
||||||
|
map.addSite(new Site("V2", 1, 0, 1, "V2-CHANNEL"));
|
||||||
|
map.addSite(new Site("V3", 1, 0, 2, "V3-CHANNEL"));
|
||||||
|
map.addSite(new Site("BLOCKER", 1, 1, 0, "TARGET-CHANNEL"));
|
||||||
|
map.addSite(new Site("TARGET", 1, 2, 0, "TARGET-CHANNEL"));
|
||||||
|
map.addSite(new Site("CACHE", 1, 1, 1, "CACHE"));
|
||||||
|
map.connect("A", "BLOCKER");
|
||||||
|
map.connect("BLOCKER", "TARGET");
|
||||||
|
map.connect("BLOCKER", "CACHE");
|
||||||
|
map.connect("V2", "A");
|
||||||
|
map.connect("V3", "V2");
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DispatchTask task() {
|
||||||
|
return new DispatchTask(
|
||||||
|
"TASK-1",
|
||||||
|
DispatchTask.TaskType.OUTBOUND,
|
||||||
|
"A",
|
||||||
|
"TARGET",
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VehicleSnapshot vehicle(String id, String site) {
|
||||||
|
return new VehicleSnapshot(id, 1, site, 5, 1, 80, true, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final InMemoryWcsState state;
|
||||||
|
private final QueueAdmissionService admission;
|
||||||
|
private final SimulatedProtocolAdapter protocol;
|
||||||
|
private final LegacyDispatchCenter center;
|
||||||
|
|
||||||
|
private Fixture(
|
||||||
|
InMemoryWcsState state,
|
||||||
|
QueueAdmissionService admission,
|
||||||
|
SimulatedProtocolAdapter protocol,
|
||||||
|
LegacyDispatchCenter center
|
||||||
|
) {
|
||||||
|
this.state = state;
|
||||||
|
this.admission = admission;
|
||||||
|
this.protocol = protocol;
|
||||||
|
this.center = center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user