feat(javaTest): add point-to-point simulation service
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package com.ft546.wcs.shadow.history.simulation;
|
||||
|
||||
import com.ft546.wcs.demo.model.DispatchTask;
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 将审计台的一次 A 到 B 请求转换为仿真器可执行的单任务输入。
|
||||
*
|
||||
* <p>该工厂位于 simulation 包中,只负责构建领域输入,不执行选车或路径规划。
|
||||
*/
|
||||
public final class PointToPointSimulationInputFactory {
|
||||
public static final String TASK_ID = "VISUALIZER-TASK-1";
|
||||
private static final LocalDateTime CREATED_AT = LocalDateTime.of(2026, 8, 5, 0, 0);
|
||||
private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 基于权威地图和初始车队创建唯一的审计任务。
|
||||
*/
|
||||
public HistoricalSimulationInput create(
|
||||
HistoricalSimulationInput base, String sourceSite, String targetSite,
|
||||
DispatchTask.TaskType taskType, int priority, String historicalVehicleId,
|
||||
List<String> historicalPath) {
|
||||
if (base == null || taskType == null) {
|
||||
throw new IllegalArgumentException("BAD_REQUEST: base input and task type are required");
|
||||
}
|
||||
if (base.getGridMap().getSite(sourceSite) == null) {
|
||||
throw new IllegalArgumentException("UNKNOWN_SITE: source " + sourceSite);
|
||||
}
|
||||
if (base.getGridMap().getSite(targetSite) == null) {
|
||||
throw new IllegalArgumentException("UNKNOWN_SITE: target " + targetSite);
|
||||
}
|
||||
if (sourceSite.equals(targetSite)) {
|
||||
throw new IllegalArgumentException("SAME_ENDPOINT: source and target must differ");
|
||||
}
|
||||
|
||||
List<String> history = historicalPath == null
|
||||
? Collections.emptyList() : historicalPath;
|
||||
String pathInfo = String.join(",", history);
|
||||
HistoricalQueueRecord record = new HistoricalQueueRecord(
|
||||
TASK_ID, TASK_ID, taskTypeCode(taskType), priority,
|
||||
sourceSite, targetSite, pathInfo,
|
||||
TIME.format(CREATED_AT), TIME.format(CREATED_AT.plusMinutes(1)),
|
||||
historicalVehicleId);
|
||||
HistoricalSimulationTask task = new HistoricalSimulationTask(
|
||||
TASK_ID, record, taskType, priority, CREATED_AT,
|
||||
CREATED_AT.plusMinutes(1), sourceSite, targetSite, history, 0);
|
||||
return new HistoricalSimulationInput(
|
||||
Collections.singletonList(task), Collections.emptyList(),
|
||||
base.getGridMap(), base.getTrafficMap(), base.getInitialVehicles());
|
||||
}
|
||||
|
||||
private String taskTypeCode(DispatchTask.TaskType taskType) {
|
||||
switch (taskType) {
|
||||
case INBOUND: return "1";
|
||||
case OUTBOUND: return "2";
|
||||
case RELOCATION: return "3";
|
||||
default: throw new IllegalArgumentException("INVALID_TASK_TYPE: " + taskType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.ft546.wcs.shadow.history.visualizer;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.ft546.wcs.demo.model.DispatchTask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 描述审计台发起的一次 A 到 B 单任务仿真请求。
|
||||
*
|
||||
* <p>请求中的历史车辆和历史路径只用于结果对照;调度中心仍会基于当前车辆快照重新选车,
|
||||
* 并在完整地图上重新规划路径。
|
||||
*/
|
||||
public final class PointToPointSimulationRequest {
|
||||
/** 审计台支持的确定性故障场景。 */
|
||||
public enum FaultPreset {
|
||||
NONE,
|
||||
PLC_FIRST_SEND_FAILURE,
|
||||
VEHICLE_RESTART,
|
||||
LOW_BATTERY,
|
||||
DISCONNECT_RECONNECT
|
||||
}
|
||||
|
||||
@JsonProperty("sourceSite")
|
||||
private String sourceSite;
|
||||
@JsonProperty("targetSite")
|
||||
private String targetSite;
|
||||
@JsonProperty("taskType")
|
||||
private String taskType;
|
||||
@JsonProperty("priority")
|
||||
private Integer priority;
|
||||
@JsonProperty("acceleration")
|
||||
private Integer acceleration;
|
||||
@JsonProperty("historicalVehicleId")
|
||||
private String historicalVehicleId;
|
||||
@JsonProperty("historicalPath")
|
||||
private List<String> historicalPath;
|
||||
@JsonProperty("faultPreset")
|
||||
private String faultPreset;
|
||||
|
||||
/** 供 Jackson 反序列化请求体使用。 */
|
||||
public PointToPointSimulationRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并立即校验一个请求,供测试和非 HTTP 调用方使用。
|
||||
*/
|
||||
public static PointToPointSimulationRequest valid(
|
||||
String sourceSite, String targetSite, String taskType,
|
||||
int priority, int acceleration, String historicalVehicleId,
|
||||
List<String> historicalPath, String faultPreset) {
|
||||
PointToPointSimulationRequest request = new PointToPointSimulationRequest();
|
||||
request.sourceSite = sourceSite;
|
||||
request.targetSite = targetSite;
|
||||
request.taskType = taskType;
|
||||
request.priority = priority;
|
||||
request.acceleration = acceleration;
|
||||
request.historicalVehicleId = historicalVehicleId;
|
||||
request.historicalPath = historicalPath;
|
||||
request.faultPreset = faultPreset;
|
||||
request.validate();
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并规范化来自页面的业务输入,避免非法参数进入调度器。
|
||||
*/
|
||||
public void validate() {
|
||||
sourceSite = required(sourceSite, "sourceSite");
|
||||
targetSite = required(targetSite, "targetSite");
|
||||
taskType = required(taskType, "taskType").toUpperCase();
|
||||
faultPreset = faultPreset == null || faultPreset.trim().isEmpty()
|
||||
? FaultPreset.NONE.name() : faultPreset.trim().toUpperCase();
|
||||
if (priority == null || priority < 0 || priority > 9) {
|
||||
throw new IllegalArgumentException("INVALID_PRIORITY: priority must be between 0 and 9");
|
||||
}
|
||||
if (acceleration == null || (acceleration != 10 && acceleration != 100)) {
|
||||
throw new IllegalArgumentException("INVALID_ACCELERATION: acceleration must be 10 or 100");
|
||||
}
|
||||
try {
|
||||
DispatchTask.TaskType.valueOf(taskType);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IllegalArgumentException("INVALID_TASK_TYPE: " + taskType, exception);
|
||||
}
|
||||
try {
|
||||
FaultPreset.valueOf(faultPreset);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IllegalArgumentException("INVALID_FAULT_PRESET: " + faultPreset, exception);
|
||||
}
|
||||
historicalVehicleId = trimToNull(historicalVehicleId);
|
||||
historicalPath = immutablePath(historicalPath);
|
||||
}
|
||||
|
||||
private static String required(String value, String name) {
|
||||
String normalized = trimToNull(value);
|
||||
if (normalized == null) {
|
||||
throw new IllegalArgumentException("BAD_REQUEST: " + name + " is required");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
if (value == null) return null;
|
||||
String normalized = value.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
private static List<String> immutablePath(List<String> source) {
|
||||
if (source == null || source.isEmpty()) return Collections.emptyList();
|
||||
List<String> result = new ArrayList<String>();
|
||||
for (String node : source) {
|
||||
String normalized = trimToNull(node);
|
||||
if (normalized != null) result.add(normalized);
|
||||
}
|
||||
return Collections.unmodifiableList(result);
|
||||
}
|
||||
|
||||
public String getSourceSite() { return sourceSite; }
|
||||
public String getTargetSite() { return targetSite; }
|
||||
public DispatchTask.TaskType getTaskType() { return DispatchTask.TaskType.valueOf(taskType); }
|
||||
public int getPriority() { return priority; }
|
||||
public int getAcceleration() { return acceleration; }
|
||||
public String getHistoricalVehicleId() { return historicalVehicleId; }
|
||||
public List<String> getHistoricalPath() { return historicalPath; }
|
||||
public FaultPreset getFaultPreset() { return FaultPreset.valueOf(faultPreset); }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ft546.wcs.shadow.history.visualizer;
|
||||
|
||||
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||
import com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulator;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
|
||||
import com.ft546.wcs.shadow.history.simulation.PointToPointSimulationInputFactory;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationConfig;
|
||||
|
||||
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.CONNECT;
|
||||
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.DISCONNECT;
|
||||
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.LOW_BATTERY;
|
||||
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.RESTART;
|
||||
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.RESTORE_BATTERY;
|
||||
import static com.ft546.wcs.shadow.history.simulation.DeterministicFaultPlan.VehicleEventType.SYNCHRONIZE;
|
||||
|
||||
/** 调用现有中心调度器执行审计台单任务仿真。 */
|
||||
public final class PointToPointSimulationService {
|
||||
private final HistoricalSimulationInput baseInput;
|
||||
private final PointToPointSimulationInputFactory inputFactory;
|
||||
private final HistoricalDispatchSimulator simulator;
|
||||
|
||||
public PointToPointSimulationService(HistoricalSimulationInput baseInput) {
|
||||
if (baseInput == null) {
|
||||
throw new IllegalArgumentException("base input is required");
|
||||
}
|
||||
this.baseInput = baseInput;
|
||||
this.inputFactory = new PointToPointSimulationInputFactory();
|
||||
this.simulator = new HistoricalDispatchSimulator();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验页面输入,构造一个任务,并完整执行选车、规划、资源、PLC、移动和归档流程。
|
||||
*/
|
||||
public HistoricalDispatchSimulationResult run(PointToPointSimulationRequest request) {
|
||||
if (request == null) throw new IllegalArgumentException("BAD_REQUEST: request is required");
|
||||
request.validate();
|
||||
HistoricalSimulationInput input = inputFactory.create(
|
||||
baseInput, request.getSourceSite(), request.getTargetSite(),
|
||||
request.getTaskType(), request.getPriority(),
|
||||
request.getHistoricalVehicleId(), request.getHistoricalPath());
|
||||
SimulationConfig config = new SimulationConfig(
|
||||
request.getAcceleration(), 30_000L, 3, 500, 20_000);
|
||||
return simulator.simulate(input, config, faultPlan(request.getFaultPreset()));
|
||||
}
|
||||
|
||||
private DeterministicFaultPlan faultPlan(PointToPointSimulationRequest.FaultPreset preset) {
|
||||
if (preset == PointToPointSimulationRequest.FaultPreset.NONE) {
|
||||
return DeterministicFaultPlan.none();
|
||||
}
|
||||
DeterministicFaultPlan.Builder builder = DeterministicFaultPlan.builder();
|
||||
if (preset == PointToPointSimulationRequest.FaultPreset.PLC_FIRST_SEND_FAILURE) {
|
||||
return builder.plcFailure(PointToPointSimulationInputFactory.TASK_ID, 1,
|
||||
SimulatedPlcGateway.PlcReason.SEND_FAILURE).build();
|
||||
}
|
||||
for (VehicleSnapshot vehicle : baseInput.getInitialVehicles()) {
|
||||
String vehicleId = vehicle.getVehicleId();
|
||||
if (preset == PointToPointSimulationRequest.FaultPreset.VEHICLE_RESTART) {
|
||||
builder.vehicleEvent(0, vehicleId, RESTART)
|
||||
.vehicleEvent(2, vehicleId, SYNCHRONIZE);
|
||||
} else if (preset == PointToPointSimulationRequest.FaultPreset.LOW_BATTERY) {
|
||||
builder.vehicleEvent(0, vehicleId, LOW_BATTERY)
|
||||
.vehicleEvent(2, vehicleId, RESTORE_BATTERY);
|
||||
} else if (preset == PointToPointSimulationRequest.FaultPreset.DISCONNECT_RECONNECT) {
|
||||
builder.vehicleEvent(0, vehicleId, DISCONNECT)
|
||||
.vehicleEvent(2, vehicleId, CONNECT);
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.ft546.wcs.shadow.history.visualizer;
|
||||
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
|
||||
import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInputFactory;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationTask;
|
||||
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationTaskResult;
|
||||
import com.ft546.wcs.shadow.history.simulation.SimulationTaskStatus;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
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 PointToPointSimulationServiceTest {
|
||||
private static HistoricalSimulationInput baseInput;
|
||||
private static HistoricalSimulationTask sampleTask;
|
||||
|
||||
@BeforeAll
|
||||
static void loadBaseInput() {
|
||||
HistoricalQueueDataset dataset = new HistoricalQueueLoader().load(
|
||||
PointToPointSimulationServiceTest.class.getClassLoader(), "shadow/history");
|
||||
baseInput = new HistoricalSimulationInputFactory().create(dataset);
|
||||
sampleTask = baseInput.getTasks().stream()
|
||||
.filter(task -> !task.getPhysicalSource().equals(task.getPhysicalTarget()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("a point-to-point sample is required"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runsOneTaskWithTheRealPlannerAndIgnoresHistoricalPath() {
|
||||
PointToPointSimulationRequest request = PointToPointSimulationRequest.valid(
|
||||
sampleTask.getPhysicalSource(), sampleTask.getPhysicalTarget(),
|
||||
"INBOUND", 1, 100, "HISTORY-CAR",
|
||||
Arrays.asList("WRONG", "REFERENCE"), "NONE");
|
||||
|
||||
HistoricalDispatchSimulationResult result = service().run(request);
|
||||
SimulationTaskResult task = result.getTaskResults().get(0);
|
||||
|
||||
assertEquals(1, result.getTaskResults().size());
|
||||
assertEquals(SimulationTaskStatus.COMPLETED, task.getStatus());
|
||||
assertEquals(sampleTask.getPhysicalSource(), task.getPlannedPath().get(0));
|
||||
assertEquals(sampleTask.getPhysicalTarget(),
|
||||
task.getPlannedPath().get(task.getPlannedPath().size() - 1));
|
||||
assertFalse(task.getPlannedPath().equals(Arrays.asList("WRONG", "REFERENCE")));
|
||||
assertEquals("HISTORY-CAR", task.getRecord().getAgvId());
|
||||
assertEquals(0, result.getInvariants().totalSafetyViolations());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownAndIdenticalEndpointsBeforeSimulation() {
|
||||
PointToPointSimulationRequest unknown = PointToPointSimulationRequest.valid(
|
||||
"UNKNOWN-SITE", sampleTask.getPhysicalTarget(), "INBOUND", 1,
|
||||
100, null, Collections.emptyList(), "NONE");
|
||||
PointToPointSimulationRequest identical = PointToPointSimulationRequest.valid(
|
||||
sampleTask.getPhysicalSource(), sampleTask.getPhysicalSource(), "INBOUND", 1,
|
||||
100, null, Collections.emptyList(), "NONE");
|
||||
|
||||
IllegalArgumentException unknownError = assertThrows(
|
||||
IllegalArgumentException.class, () -> service().run(unknown));
|
||||
IllegalArgumentException identicalError = assertThrows(
|
||||
IllegalArgumentException.class, () -> service().run(identical));
|
||||
|
||||
assertTrue(unknownError.getMessage().contains("UNKNOWN_SITE"));
|
||||
assertTrue(identicalError.getMessage().contains("SAME_ENDPOINT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void plcFirstSendFailureProducesRetryEvidenceAndStillCompletes() {
|
||||
PointToPointSimulationRequest request = PointToPointSimulationRequest.valid(
|
||||
sampleTask.getPhysicalSource(), sampleTask.getPhysicalTarget(),
|
||||
"OUTBOUND", 2, 100, null, Collections.emptyList(),
|
||||
"PLC_FIRST_SEND_FAILURE");
|
||||
|
||||
HistoricalDispatchSimulationResult result = service().run(request);
|
||||
|
||||
assertEquals(1, result.getCompletedCount());
|
||||
assertTrue(result.getRetryCount() > 0);
|
||||
assertTrue(result.getEvents().stream()
|
||||
.anyMatch(event -> "PLC_RETRY".equals(event.getType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesPriorityAccelerationTaskTypeAndFaultPreset() {
|
||||
assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid(
|
||||
"A", "B", "INBOUND", 10, 100, null,
|
||||
Collections.emptyList(), "NONE"));
|
||||
assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid(
|
||||
"A", "B", "INBOUND", 1, 20, null,
|
||||
Collections.emptyList(), "NONE"));
|
||||
assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid(
|
||||
"A", "B", "UNKNOWN", 1, 100, null,
|
||||
Collections.emptyList(), "NONE"));
|
||||
assertThrows(IllegalArgumentException.class, () -> PointToPointSimulationRequest.valid(
|
||||
"A", "B", "INBOUND", 1, 100, null,
|
||||
Collections.emptyList(), "UNKNOWN"));
|
||||
}
|
||||
|
||||
private PointToPointSimulationService service() {
|
||||
return new PointToPointSimulationService(baseInput);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user