| New file |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertFalse; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import static org.mockito.Mockito.mock; |
| | | |
| | | import java.io.IOException; |
| | | import java.time.Duration; |
| | | import java.time.Instant; |
| | | import java.util.List; |
| | | import org.awaitility.Awaitility; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.web.context.request.async.AsyncRequestNotUsableException; |
| | | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
| | | import com.ruoyi.device.bts.BtsDevelopmentSimulator; |
| | | import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | import com.ruoyi.system.service.ITTestService; |
| | | import com.ruoyi.system.service.ITestLiveService.LiveState; |
| | | |
| | | /** 实时流客户端断连时应结束连接而不触发全局异常处理。 */ |
| | | class TTestLiveServiceImplTest |
| | | { |
| | | @Test |
| | | void initialWriteFailureShouldCompleteStreamWithoutErrorDispatch() |
| | | { |
| | | RecordingEmitter emitter = new RecordingEmitter(0); |
| | | TTestLiveServiceImpl service = new TestableLiveService(emitter); |
| | | |
| | | try |
| | | { |
| | | service.stream(4L); |
| | | } |
| | | finally |
| | | { |
| | | service.shutdown(); |
| | | } |
| | | |
| | | assertTrue(emitter.completed); |
| | | assertFalse(emitter.completedWithError); |
| | | } |
| | | |
| | | @Test |
| | | void scheduledWriteFailureShouldCompleteStreamWithoutErrorDispatch() |
| | | { |
| | | RecordingEmitter emitter = new RecordingEmitter(6); |
| | | TTestLiveServiceImpl service = new TestableLiveService(emitter); |
| | | |
| | | try |
| | | { |
| | | service.stream(4L); |
| | | Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> emitter.completed); |
| | | } |
| | | finally |
| | | { |
| | | service.shutdown(); |
| | | } |
| | | |
| | | assertFalse(emitter.completedWithError); |
| | | } |
| | | |
| | | private static final class TestableLiveService extends TTestLiveServiceImpl |
| | | { |
| | | private final RecordingEmitter emitter; |
| | | |
| | | private TestableLiveService(RecordingEmitter emitter) |
| | | { |
| | | super(mock(ITTestService.class), mock(DeviceDiscoveryCandidateStore.class), |
| | | mock(BtsDevelopmentSimulator.class), mock(TTestMapper.class)); |
| | | this.emitter = emitter; |
| | | } |
| | | |
| | | @Override |
| | | protected SseEmitter createEmitter() |
| | | { |
| | | return emitter; |
| | | } |
| | | |
| | | @Override |
| | | public LiveState current(Long testId) |
| | | { |
| | | return new LiveState(testId, "RUNNING", "RECEIVING", Instant.now(), List.of(), List.of()); |
| | | } |
| | | } |
| | | |
| | | private static final class RecordingEmitter extends SseEmitter |
| | | { |
| | | private final int successfulWrites; |
| | | private int writes; |
| | | private volatile boolean completed; |
| | | private volatile boolean completedWithError; |
| | | |
| | | private RecordingEmitter(int successfulWrites) |
| | | { |
| | | this.successfulWrites = successfulWrites; |
| | | } |
| | | |
| | | @Override |
| | | public void send(SseEventBuilder builder) throws IOException |
| | | { |
| | | if (writes++ >= successfulWrites) |
| | | throw new AsyncRequestNotUsableException("client disconnected", new IOException("connection closed")); |
| | | } |
| | | |
| | | @Override |
| | | public synchronized void complete() |
| | | { |
| | | completed = true; |
| | | } |
| | | |
| | | @Override |
| | | public synchronized void completeWithError(Throwable error) |
| | | { |
| | | completedWithError = true; |
| | | } |
| | | } |
| | | } |