package com.ruoyi.system.service.impl;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.when;
|
|
import java.io.IOException;
|
import java.math.BigDecimal;
|
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.device.model.ChannelIdentity;
|
import com.ruoyi.device.model.DeviceWorkStatus;
|
import com.ruoyi.device.model.RealtimeSnapshot;
|
import com.ruoyi.system.domain.TTest;
|
import com.ruoyi.system.domain.TTestBattery;
|
import com.ruoyi.system.domain.TTestChannelBinding;
|
import com.ruoyi.system.service.ITTestService;
|
import com.ruoyi.system.service.ITestLiveService.LiveState;
|
|
/** 实时流客户端断连时应结束连接而不触发全局异常处理。 */
|
class TTestLiveServiceImplTest
|
{
|
@Test
|
void notStartedShouldNotExposePreTestDeviceSnapshot()
|
{
|
Instant now = Instant.parse("2026-09-02T06:00:00Z");
|
TTest test = test("NOT_STARTED");
|
TTestLiveServiceImpl service = liveService(test, now);
|
|
LiveState state = service.current(10L);
|
|
assertTrue(state.sources().stream().allMatch(source -> source.latestSnapshot().isEmpty()));
|
}
|
|
@Test
|
void receivingShouldExposeBtsCoreMeasurements()
|
{
|
Instant now = Instant.parse("2026-09-02T06:00:00Z");
|
TTest test = test("RECEIVING");
|
TTestLiveServiceImpl service = liveService(test, now);
|
|
LiveState state = service.current(10L);
|
var snapshot = state.sources().stream().filter(source -> "CABINET_CHANNEL".equals(source.sourceType()))
|
.findFirst().orElseThrow().latestSnapshot();
|
|
assertEquals(new BigDecimal("3.700"), snapshot.get("voltageV"));
|
assertEquals(new BigDecimal("1.200"), snapshot.get("currentA"));
|
assertEquals(new BigDecimal("2.500"), snapshot.get("capacityAh"));
|
}
|
|
@Test
|
void onlineCabinetWithStoppedWorkStatusShouldRemainOnlineAndNotRaiseSourceAlarm()
|
{
|
Instant now = Instant.parse("2026-09-02T06:00:00Z");
|
TTestLiveServiceImpl service = liveService(test("RECEIVING"), now, true, DeviceWorkStatus.STOPPED);
|
|
LiveState state = service.current(10L);
|
var cabinet = state.sources().stream().filter(source -> "CABINET_CHANNEL".equals(source.sourceType()))
|
.findFirst().orElseThrow();
|
|
assertEquals("ONLINE", cabinet.deviceStatus());
|
assertTrue(state.exceptions().isEmpty());
|
}
|
|
@Test
|
void pausedShouldNotExposeUnavailableSourceAsAlarm()
|
{
|
Instant now = Instant.parse("2026-09-02T06:00:00Z");
|
TTest test = test("PAUSED");
|
TTestLiveServiceImpl service = liveService(test, now, false);
|
|
LiveState state = service.current(10L);
|
|
assertTrue(state.exceptions().isEmpty());
|
}
|
|
private static TTest test(String receptionStatus)
|
{
|
TTest test = new TTest();
|
test.setTestId(10L);
|
test.setTestStatus("RECEIVING".equals(receptionStatus) ? "RUNNING" : "WAITING");
|
test.setReceptionStatus(receptionStatus);
|
TTestBattery battery = new TTestBattery();
|
battery.setBatteryNo("B-01");
|
TTestChannelBinding binding = new TTestChannelBinding();
|
binding.setChannelId(1L);
|
binding.setAdapterCode("dev-sim");
|
binding.setDevtype(1);
|
binding.setDevid(1);
|
binding.setSubdevid(0);
|
binding.setChlid(1);
|
battery.setBinding(binding);
|
test.setBatteries(List.of(battery));
|
return test;
|
}
|
|
private static TTestLiveServiceImpl liveService(TTest test, Instant now)
|
{
|
return liveService(test, now, true);
|
}
|
|
private static TTestLiveServiceImpl liveService(TTest test, Instant now, boolean online)
|
{
|
return liveService(test, now, online, DeviceWorkStatus.WORKING);
|
}
|
|
private static TTestLiveServiceImpl liveService(TTest test, Instant now, boolean online,
|
DeviceWorkStatus workStatus)
|
{
|
DeviceDiscoveryCandidateStore candidates = mock(DeviceDiscoveryCandidateStore.class);
|
BtsDevelopmentSimulator simulator = mock(BtsDevelopmentSimulator.class);
|
ITTestService tests = mock(ITTestService.class);
|
when(tests.selectTestById(10L)).thenReturn(test);
|
ChannelIdentity identity = new ChannelIdentity("dev-sim", 1, 1, 0, 1);
|
RealtimeSnapshot snapshot = new RealtimeSnapshot(identity, workStatus, 1, 1, "cc", null,
|
new BigDecimal("1.200"), new BigDecimal("3.700"), new BigDecimal("2.500"),
|
new BigDecimal("9.250"), 12L, 12L, "0", now,
|
java.util.Map.of());
|
DeviceDiscoveryCandidateStore.ChannelCandidate channel = new DeviceDiscoveryCandidateStore.ChannelCandidate(
|
identity, online, workStatus, now, snapshot);
|
DeviceDiscoveryCandidateStore.CabinetCandidate cabinet = new DeviceDiscoveryCandidateStore.CabinetCandidate(
|
"dev-sim", 1L, "dev-sim", 1, 1, "127.0.0.2", "mac", "cabinet", "vendor", "model",
|
List.of(channel), now, now.plusSeconds(60));
|
when(candidates.snapshot(any(Instant.class))).thenReturn(
|
new DeviceDiscoveryCandidateStore.CandidateSnapshot(List.of(cabinet), List.of()));
|
var plan = new BtsDevelopmentSimulator.BtsTestSnapshot(1, 1, 1, List.of());
|
when(simulator.btsTest(anyInt(), anyInt())).thenReturn(plan);
|
when(simulator.btsTest(anyInt(), anyInt(), anyInt())).thenReturn(plan);
|
return new CurrentLiveService(tests, candidates, simulator);
|
}
|
|
@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));
|
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 CurrentLiveService extends TTestLiveServiceImpl
|
{
|
private CurrentLiveService(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
|
BtsDevelopmentSimulator simulator)
|
{
|
super(tests, candidates, simulator);
|
}
|
}
|
|
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;
|
}
|
}
|
}
|