fix: repair shadow candidate planning and site runtime errors
This commit is contained in:
98
ruoyi-admin/src/test/java/com/ruoyi/Dzp/DzpSendTest.java
Normal file
98
ruoyi-admin/src/test/java/com/ruoyi/Dzp/DzpSendTest.java
Normal file
@@ -0,0 +1,98 @@
|
||||
package com.ruoyi.Dzp;
|
||||
|
||||
import com.ruoyi.Dzp.domain.basLed;
|
||||
import com.ruoyi.Dzp.mapper.BasLedMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class DzpSendTest {
|
||||
@Test
|
||||
void failedExchangeClosesItsSocketAndDoesNotAcknowledgeTheDisplay() throws Exception {
|
||||
var socket = mock(MyUdpSocket.class);
|
||||
var sender = new DzpSend() { @Override MyUdpSocket openSocket() { return socket; } };
|
||||
var mapper = mock(BasLedMapper.class);
|
||||
ReflectionTestUtils.setField(sender, "mapper", mapper);
|
||||
var record = new basLed(); record.setModel("3"); record.setSendFlag("0"); record.setIpAddress("127.0.0.1"); record.setErrorMsg("alarm");
|
||||
when(mapper.selectBasLedList(any())).thenAnswer(call -> call.getArgument(0, basLed.class).getId() == 1 ? List.of(record) : List.of());
|
||||
try (var protocol = mockStatic(ProtocolDemo.class)) {
|
||||
protocol.when(() -> ProtocolDemo.demo_play_chapterschedule2(socket, "127.0.0.1", "报警信息", "alarm"))
|
||||
.thenThrow(new IllegalStateException("test send failure")).thenReturn(true);
|
||||
assertThatThrownBy(sender::ErrorLed).hasMessage("test send failure");
|
||||
verify(socket).close();
|
||||
verify(mapper, never()).updateBasLed(any());
|
||||
assertThat(record.getSendFlag()).isEqualTo("0");
|
||||
sender.ErrorLed();
|
||||
verify(socket, times(2)).close();
|
||||
verify(mapper).updateBasLed(record);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void busyLocalPortDefersOtherTimerWithoutBlockingOrChangingRows() throws Exception {
|
||||
var entered = new CountDownLatch(1);
|
||||
var release = new CountDownLatch(1);
|
||||
var sender = new DzpSend() {
|
||||
@Override MyUdpSocket openSocket() throws IOException {
|
||||
entered.countDown();
|
||||
try { if (!release.await(3, TimeUnit.SECONDS)) throw new IOException("test timeout"); }
|
||||
catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(e); }
|
||||
throw new IOException("fixture port failure");
|
||||
}
|
||||
};
|
||||
var mapper = mock(BasLedMapper.class);
|
||||
ReflectionTestUtils.setField(sender, "mapper", mapper);
|
||||
var record = new basLed(); record.setModel("1"); record.setSendFlag("0");
|
||||
when(mapper.selectBasLedList(any())).thenReturn(List.of(record));
|
||||
var executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var first = executor.submit(() -> { assertThatThrownBy(sender::InLed).isInstanceOf(IOException.class); });
|
||||
assertThat(entered.await(2, TimeUnit.SECONDS)).isTrue();
|
||||
executor.submit(() -> { sender.ErrorLed(); return null; }).get(1, TimeUnit.SECONDS);
|
||||
verify(mapper, times(1)).selectBasLedList(any());
|
||||
verify(mapper, never()).updateBasLed(any());
|
||||
release.countDown();
|
||||
first.get(2, TimeUnit.SECONDS);
|
||||
} finally { release.countDown(); executor.shutdownNow(); }
|
||||
}
|
||||
|
||||
@Test
|
||||
void eachScheduledMessageBuildsItsOwnPayload() throws Exception {
|
||||
try (var payloads = mockConstruction(LEDSender2010.class,
|
||||
(sender, context) -> when(sender.GetDataSize()).thenReturn((1 << 21) + 1))) {
|
||||
var socket = mock(MyUdpSocket.class);
|
||||
assertThat(ProtocolDemo.demo_play_chapterschedule2(socket, "127.0.0.1", "title1", "one")).isFalse();
|
||||
assertThat(ProtocolDemo.demo_play_chapterschedule2(socket, "127.0.0.1", "title2", "two")).isFalse();
|
||||
assertThat(payloads.constructed()).hasSize(2);
|
||||
verify(payloads.constructed().get(0)).GetDataSize();
|
||||
verify(payloads.constructed().get(1)).GetDataSize();
|
||||
verifyNoInteractions(socket);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void transientSocketErrorsStayInsideTheBoundedProtocolRetry() throws Exception {
|
||||
var socket = mock(MyUdpSocket.class);
|
||||
var payload = mock(LEDSender2010.class);
|
||||
doThrow(new IOException("transient send")).doNothing().when(socket).send(anyString(), anyInt(), any(), anyInt());
|
||||
when(socket.receive()).thenThrow(new java.io.UncheckedIOException(new IOException("transient receive"))).thenReturn(1);
|
||||
when(payload.parse_trans_respond(any(), anyInt(), anyInt(), anyInt())).thenReturn(1);
|
||||
assertThat(ProtocolDemo.send_data1(socket, payload, "127.0.0.1")).isTrue();
|
||||
verify(socket, times(3)).send(anyString(), anyInt(), any(), anyInt());
|
||||
verify(socket, times(3)).receive();
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistentSendFailureStopsAfterFiveAttempts() throws Exception {
|
||||
var socket = mock(MyUdpSocket.class);
|
||||
doThrow(new IOException("unreachable")).when(socket).send(anyString(), anyInt(), any(), anyInt());
|
||||
assertThat(ProtocolDemo.send_data1(socket, mock(LEDSender2010.class), "127.0.0.1")).isFalse();
|
||||
verify(socket, times(5)).send(anyString(), anyInt(), any(), anyInt());
|
||||
verify(socket, never()).receive();
|
||||
}
|
||||
}
|
||||
50
ruoyi-admin/src/test/java/com/ruoyi/Dzp/MyUdpSocketTest.java
Normal file
50
ruoyi-admin/src/test/java/com/ruoyi/Dzp/MyUdpSocketTest.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.Dzp;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.InetAddress;
|
||||
import java.io.IOException;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class MyUdpSocketTest {
|
||||
@Test
|
||||
void occupiedPortFailsConstructionInsteadOfReturningBrokenSocket() throws Exception {
|
||||
try (var occupied = new DatagramSocket(0)) {
|
||||
assertThatThrownBy(() -> new MyUdpSocket(occupied.getLocalPort()))
|
||||
.isInstanceOf(IOException.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivedPeerBelongsToItsOwnSocket() throws Exception {
|
||||
MyUdpSocket first = new MyUdpSocket(0);
|
||||
MyUdpSocket second = new MyUdpSocket(0);
|
||||
try (var peerOne = new DatagramSocket(0); var peerTwo = new DatagramSocket(0)) {
|
||||
exchange(first, peerOne, (byte) 11);
|
||||
int firstPeer = first.get_client_port();
|
||||
exchange(second, peerTwo, (byte) 22);
|
||||
assertThat(first.get_client_port()).isEqualTo(firstPeer);
|
||||
assertThat(second.get_client_port()).isEqualTo(peerTwo.getLocalPort());
|
||||
first.close();
|
||||
exchange(second, peerTwo, (byte) 33);
|
||||
} finally {
|
||||
first.close();
|
||||
second.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void exchange(MyUdpSocket socket, DatagramSocket peer, byte value) throws Exception {
|
||||
peer.setSoTimeout(1000);
|
||||
socket.send("127.0.0.1", peer.getLocalPort(), new byte[]{value}, 1);
|
||||
var request = new DatagramPacket(new byte[64], 64);
|
||||
peer.receive(request);
|
||||
peer.send(new DatagramPacket(new byte[]{value}, 1, InetAddress.getLoopbackAddress(), request.getPort()));
|
||||
int size = 0;
|
||||
for (int attempt = 0; attempt < 100 && size == 0; attempt++) size = socket.receive();
|
||||
assertThat(size).isEqualTo(1);
|
||||
byte[] result = new byte[1];
|
||||
socket.get_receive_packet(result, 1);
|
||||
assertThat(result).containsExactly(value);
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,23 @@ class ShadowInboundEligibilityTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleProductsWithOneFieldGroupCanContinueTheMatchingProjectLane() {
|
||||
jdbc.update("update alg.shadow_inbound_request_snapshot set detail_count=5,product_scope_count=5,product_id=null,lot_no=null");
|
||||
space("RESIDENT", "LANE", "1", "2", "N", "OLD", "C");
|
||||
stock("RESIDENT", "OLD");
|
||||
assertThat(eligible()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ambiguousFieldGroupsOnlyAllowEntirelyEmptyLanes() {
|
||||
jdbc.update("update alg.shadow_inbound_request_snapshot set detail_count=5,field_group_count=2,product_scope_count=5");
|
||||
assertThat(eligible()).isEqualTo(1);
|
||||
space("RESIDENT", "LANE", "1", "2", "N", "OLD", "C");
|
||||
stock("RESIDENT", "OLD");
|
||||
assertThat(eligible()).isZero();
|
||||
}
|
||||
|
||||
private void space(String id, String lane, String floor, String status, String locked, String tray, String area) {
|
||||
jdbc.update("insert into alg.cargo_space_snapshot values ('01',42,?,?,?,?,?,?,?,'UNRELATED')",
|
||||
id, lane, floor, status, locked, tray, area);
|
||||
|
||||
@@ -61,6 +61,7 @@ class ShadowInboundValidationTest {
|
||||
reference.setTrayJobCount(1);
|
||||
reference.setLegacyScopeComplete(1);
|
||||
when(mapper.selectInboundInput("01", 42, "31331", "TRAY-1")).thenReturn(reference);
|
||||
when(mapper.selectInboundEligibleSpaces("01", 42, "31331", "TRAY-1")).thenReturn(java.util.List.of("SPACE-9"));
|
||||
when(mapper.countInboundEligibleSpace("01", 42, "31331", "TRAY-1", "SPACE-9")).thenReturn(1);
|
||||
var assignment = new InboundAssignment("HPR-1", "SPACE-9", "LANE", 1, 1,
|
||||
InboundLadderRung.CONTINUE, InboundBorrowType.NONE, null,
|
||||
@@ -87,13 +88,22 @@ class ShadowInboundValidationTest {
|
||||
reference.setDetailCount(45);
|
||||
reference.setFieldGroupCount(2);
|
||||
reference.setProductScopeCount(45);
|
||||
assertSkipped("SKIPPED_AMBIGUOUS_LEGACY_FIELDS");
|
||||
var result = plan();
|
||||
assertThat(result.resultCode()).isEqualTo("READY_CONSERVATIVE");
|
||||
assertThat(result.reason()).contains("legacyEligibility=EMPTY_LANE_ONLY");
|
||||
engine.compareInbound(result, "10", "SPACE-9");
|
||||
verify(mapper).insertComparison(any(), any(), eq("INBOUND_COMPARE"), anyLong(), any(), any(),
|
||||
eq("MATCH_CONSERVATIVE"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiProductTrayDoesNotSilentlyUseOneRepresentativeProduct() {
|
||||
reference.setProductScopeCount(2);
|
||||
assertSkipped("SKIPPED_MULTI_PRODUCT_INPUT");
|
||||
assertThat(plan().resultCode()).isEqualTo("READY");
|
||||
verify(facade).planInboundFromPublishedSnapshot(eq("01"), argThat(request ->
|
||||
request.productId() == null && request.lotNo() == null
|
||||
&& request.batchLaneMode() == com.ruoyi.algorithm.inbound.BatchLaneMode.OPEN
|
||||
&& request.eligibleSpaceIds().equals(java.util.Set.of("SPACE-9"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -148,6 +158,12 @@ class ShadowInboundValidationTest {
|
||||
|
||||
private InboundShadowObservation plan() { return engine.planInbound(snapshot, "31331", "TRAY-1"); }
|
||||
|
||||
@Test
|
||||
void noConservativeSpaceDoesNotInventAnUnrestrictedRequest() {
|
||||
when(mapper.selectInboundEligibleSpaces("01", 42, "31331", "TRAY-1")).thenReturn(java.util.List.of());
|
||||
assertSkipped("SKIPPED_NO_CONSERVATIVE_SPACE");
|
||||
}
|
||||
|
||||
private void assertSkipped(String code) {
|
||||
var result = plan();
|
||||
assertThat(result.resultCode()).isEqualTo(code);
|
||||
|
||||
@@ -59,7 +59,7 @@ class ShadowOutboundValidationTest {
|
||||
candidate.setTrayJobCount(0);
|
||||
when(mapper.selectProjectOutboundReference("01", 459L, "DETAIL")).thenReturn(input);
|
||||
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL")).thenReturn(List.of(candidate));
|
||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any())).thenReturn(
|
||||
when(facade.planShadowOutboundCandidates(eq("01"), any(), any())).thenReturn(
|
||||
OutboundPlan.ready(new OutboundAssignment("DETAIL", "TRAY", input.getProjectNo(), "PRODUCT",
|
||||
"SPACE", 1, false, List.of(), List.of(), false, 0, 0, 0,
|
||||
FifoDecision.off(), Optional.empty(), "proposal"), TaskCountEstimate.zero()));
|
||||
@@ -71,7 +71,7 @@ class ShadowOutboundValidationTest {
|
||||
assertThat(result.resultCode()).isEqualTo("READY");
|
||||
assertThat(result.proposedStockId()).isEqualTo("STOCK");
|
||||
assertThat(result.reason()).contains("predicateVersion=1", "requestedQuantity=3", "selectedProject=AD.L-00033.01.01");
|
||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), argThat(r -> !r.wholeTrayOrDefault()));
|
||||
verify(facade).planShadowOutboundCandidates(eq("01"), argThat(r -> !r.wholeTrayOrDefault()), eq(java.util.Set.of("STOCK")));
|
||||
verify(mapper, never()).refreshSnapshot(anyString(), anyLong(), anyString());
|
||||
verify(facade, never()).planOutbound(anyString(), any());
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class ShadowOutboundValidationTest {
|
||||
void emptyProjectDerivesOnlyProvenCandidateScope() {
|
||||
input.setProjectNo(null);
|
||||
assertThat(plan().resultCode()).isEqualTo("READY");
|
||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), argThat(r -> "AD.L-00033.01.01".equals(r.projectNo())));
|
||||
verify(facade).planShadowOutboundCandidates(eq("01"), argThat(r -> "AD.L-00033.01.01".equals(r.projectNo())), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,9 +134,12 @@ class ShadowOutboundValidationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleRowsOnSelectedTrayAreExplicitlyUnsupported() {
|
||||
void otherRowsOnSelectedTrayDoNotEraseTheEligibleItem() {
|
||||
candidate.setTrayItemCount(2);
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_MULTI_STOCK_TRAY");
|
||||
var result = plan();
|
||||
assertThat(result.resultCode()).isEqualTo("READY");
|
||||
assertThat(result.proposedStockId()).isEqualTo("STOCK");
|
||||
assertThat(result.reason()).contains("candidateEvidenceComplete=true", "\"trayItems\":2", "\"stock\":\"STOCK\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -155,4 +158,13 @@ class ShadowOutboundValidationTest {
|
||||
private OutboundShadowObservation plan() {
|
||||
return engine.planProjectOutbound(snapshot, "DETAIL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedDuplicateEligibleRowsRemainExplicitAndKeepEvidence() {
|
||||
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL")).thenReturn(List.of(candidate, candidate));
|
||||
var result = plan();
|
||||
assertThat(result.resultCode()).isEqualTo("SKIPPED_AMBIGUOUS_STOCK_SCOPE");
|
||||
assertThat(result.reason()).contains("candidateCount=2", "candidateSha256=");
|
||||
verifyNoInteractions(facade);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class ShadowPlanningEngineTest {
|
||||
ShadowInboundReference input = inboundReference("1");
|
||||
input.setShadowCandidateCount(3);
|
||||
when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1")).thenReturn(input);
|
||||
when(mapper.selectInboundEligibleSpaces("01", 42L, "TASK-1", "TRAY-1")).thenReturn(java.util.List.of("SPACE-9"));
|
||||
when(mapper.countInboundEligibleSpace("01", 42L, "TASK-1", "TRAY-1", "SPACE-9")).thenReturn(1);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
when(facade.planInboundFromPublishedSnapshot(eq("01"), any()))
|
||||
@@ -89,7 +90,7 @@ class ShadowPlanningEngineTest {
|
||||
.thenReturn(java.util.List.of(stockReference("01")));
|
||||
when(mapper.selectProjectOutboundReference("01", 42L, "DETAIL-1")).thenReturn(reference);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any()))
|
||||
when(facade.planShadowOutboundCandidates(eq("01"), any(), any()))
|
||||
.thenReturn(OutboundPlan.ready(outboundAssignment(), TaskCountEstimate.zero()));
|
||||
ShadowPlanningEngine engine = engine(facade, mapper);
|
||||
|
||||
@@ -104,7 +105,7 @@ class ShadowPlanningEngineTest {
|
||||
"sourceSignature=ABC123", "detail=DETAIL-1", "rawWarehouse=W02",
|
||||
"project=PROJECT-1", "product=PRODUCT-1", "shadowMatches=1");
|
||||
verify(mapper).selectProjectOutboundReference("01", 42L, "DETAIL-1");
|
||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), any());
|
||||
verify(facade).planShadowOutboundCandidates(eq("01"), any(), eq(java.util.Set.of("STOCK-1")));
|
||||
verify(facade, never()).planOutbound(any(), any());
|
||||
}
|
||||
|
||||
@@ -359,6 +360,8 @@ class ShadowPlanningEngineTest {
|
||||
}
|
||||
|
||||
private static ShadowPlanningEngine engine(SchedulingFacade facade, ShadowInputMapper mapper) {
|
||||
when(mapper.selectInboundEligibleSpaces(any(), any(Long.class), any(), any()))
|
||||
.thenReturn(java.util.List.of("SPACE-9"));
|
||||
return new ShadowPlanningEngine(facade, mapper, properties(), new CapturingTransactionManager());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ruoyi.in.stockindetail.controller;
|
||||
|
||||
import com.ruoyi.framework.web.exception.GlobalExceptionHandler;
|
||||
import com.ruoyi.in.stockindetail.service.IInStockinDetailService;
|
||||
import com.ruoyi.in.stockindetailfinish.service.IInStockinDetailFinishService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
class InStockinDetailBindingTest {
|
||||
@org.springframework.web.bind.annotation.RestController
|
||||
static class BrokenServer {
|
||||
@org.springframework.web.bind.annotation.GetMapping("/fixture/server-failure")
|
||||
Object get() { throw new org.springframework.beans.InvalidPropertyException(BrokenServer.class,
|
||||
"stockinId[limit]", "server property failure"); }
|
||||
}
|
||||
|
||||
@Test
|
||||
void serverPropertyFailureIsNotRelabeledAsBadClientInput() throws Exception {
|
||||
MockMvcBuilders.standaloneSetup(new BrokenServer()).setControllerAdvice(new GlobalExceptionHandler()).build()
|
||||
.perform(get("/fixture/server-failure").param("stockinId[limit]", "20"))
|
||||
.andExpect(jsonPath("$.code").value(500));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedOrderNumberIsRejectedBeforeEitherDatabaseQuery() throws Exception {
|
||||
var controller = new InStockinDetailController();
|
||||
var active = mock(IInStockinDetailService.class);
|
||||
var finished = mock(IInStockinDetailFinishService.class);
|
||||
ReflectionTestUtils.setField(controller, "inStockinDetailService", active);
|
||||
ReflectionTestUtils.setField(controller, "inStockinDetailFinishService", finished);
|
||||
var mvc = MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new GlobalExceptionHandler()).build();
|
||||
|
||||
mvc.perform(get("/in/stockInDetail/list").param("stockinId[limit]", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(400))
|
||||
.andExpect(jsonPath("$.msg").value("请求参数格式不正确,单号等普通字段请传单个值,不要传对象或数组"));
|
||||
verifyNoInteractions(active, finished);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user