feat(wcs): add deterministic vehicle assignment
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package com.ruoyi.wcs.domain.dispatch;
|
||||
|
||||
import com.ruoyi.wcs.domain.equipment.DeviceCapability;
|
||||
import com.ruoyi.wcs.domain.equipment.DeviceDescriptor;
|
||||
import com.ruoyi.wcs.domain.equipment.DeviceType;
|
||||
import com.ruoyi.wcs.domain.equipment.VehicleSnapshot;
|
||||
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
|
||||
import com.ruoyi.wcs.domain.topology.ResourceDefinition;
|
||||
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.AStarRoutePlanner;
|
||||
import com.ruoyi.wcs.port.out.DispatchPolicy;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
|
||||
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 DefaultVehicleAssignmentPolicyTest {
|
||||
private final EffectiveTopology topology = topology();
|
||||
private final VehicleAssignmentPolicy policy = new DefaultVehicleAssignmentPolicy(
|
||||
new AStarRoutePlanner(), DispatchPolicy.NONE);
|
||||
|
||||
@Test
|
||||
void rejectsCloserVehicleWithoutRequiredCapability() {
|
||||
VehicleAssignmentDecision decision = policy.assign(taskRequiringLoad(),
|
||||
Arrays.asList(
|
||||
vehicle("V1", "B", 90, false, VehicleSnapshot.ConnectionState.ONLINE),
|
||||
vehicle("V2", "A", 70, true, VehicleSnapshot.ConnectionState.ONLINE)),
|
||||
topology);
|
||||
|
||||
assertEquals("V2", decision.getVehicleId());
|
||||
assertTrue(decision.getReasonCodes().contains("CAPABILITY_MATCH"));
|
||||
assertEquals(Arrays.asList("A", "B", "C", "D"),
|
||||
decision.getRoute().getNodeIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
void breaksEqualScoresByVehicleId() {
|
||||
VehicleAssignmentDecision decision = policy.assign(taskWithoutLoad(),
|
||||
Arrays.asList(
|
||||
vehicle("V2", "A", 80, true, VehicleSnapshot.ConnectionState.ONLINE),
|
||||
vehicle("V1", "A", 80, true, VehicleSnapshot.ConnectionState.ONLINE)),
|
||||
topology);
|
||||
|
||||
assertEquals("V1", decision.getVehicleId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void appliesHardDispatchabilityFilterBeforeScoring() {
|
||||
VehicleAssignmentDecision decision = policy.assign(taskWithoutLoad(),
|
||||
Arrays.asList(
|
||||
vehicle("V1", "C", 100, true, VehicleSnapshot.ConnectionState.OFFLINE),
|
||||
vehicle("V2", "A", 40, true, VehicleSnapshot.ConnectionState.ONLINE)),
|
||||
topology);
|
||||
|
||||
assertEquals("V2", decision.getVehicleId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsNoAssignmentWhenEveryVehicleFailsAHardCondition() {
|
||||
VehicleAssignmentDecision decision = policy.assign(taskRequiringLoad(),
|
||||
Collections.singletonList(vehicle("V1", "B", 90, false,
|
||||
VehicleSnapshot.ConnectionState.ONLINE)), topology);
|
||||
|
||||
assertFalse(decision.isAssigned());
|
||||
assertEquals(Collections.singletonList("NO_ELIGIBLE_VEHICLE"),
|
||||
decision.getReasonCodes());
|
||||
}
|
||||
|
||||
private static DispatchTask taskRequiringLoad() {
|
||||
return new DispatchTask("O1", "S1", "C", "D", 10, 60L, 30,
|
||||
Collections.singleton(DeviceCapability.LOAD_HANDLING));
|
||||
}
|
||||
|
||||
private static DispatchTask taskWithoutLoad() {
|
||||
return new DispatchTask("O1", "S1", "C", "D", 10, 60L, 30,
|
||||
Collections.<DeviceCapability>emptySet());
|
||||
}
|
||||
|
||||
private static VehicleSnapshot vehicle(String id, String location, int battery,
|
||||
boolean loadCapability,
|
||||
VehicleSnapshot.ConnectionState connection) {
|
||||
EnumSet<DeviceCapability> capabilities =
|
||||
EnumSet.of(DeviceCapability.FOUR_WAY_MOVE);
|
||||
if (loadCapability) {
|
||||
capabilities.add(DeviceCapability.LOAD_HANDLING);
|
||||
}
|
||||
DeviceDescriptor descriptor = new DeviceDescriptor(id,
|
||||
DeviceType.FOUR_WAY_SHUTTLE, "F1", capabilities);
|
||||
return new VehicleSnapshot(descriptor, connection,
|
||||
VehicleSnapshot.OperatingMode.AUTO,
|
||||
VehicleSnapshot.HealthState.NORMAL,
|
||||
VehicleSnapshot.OccupationState.IDLE,
|
||||
location, null, battery, null, "F1",
|
||||
Instant.parse("2026-08-04T00:00:00Z"));
|
||||
}
|
||||
|
||||
private static EffectiveTopology topology() {
|
||||
TopologyDefinition definition = new TopologyDefinition("FT546", 7L, true,
|
||||
Arrays.asList(node("A", 0), node("B", 1), node("C", 2), node("D", 3)),
|
||||
Arrays.asList(edge("A", "B"), edge("B", "C"), edge("C", "D")),
|
||||
Collections.<ResourceDefinition>emptyList(),
|
||||
Collections.<String>emptyList(), Collections.<String>emptyList());
|
||||
return new TopologyCompiler().compile(definition);
|
||||
}
|
||||
|
||||
private static TopologyNode node(String id, int x) {
|
||||
return new TopologyNode(id, "F1", x, 0, 1, false, false);
|
||||
}
|
||||
|
||||
private static TopologyEdge edge(String source, String target) {
|
||||
return new TopologyEdge(source, target, 1, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user