package com.battery.business.runtime;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.clearInvocations;
|
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
import java.util.ArrayList;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.concurrent.atomic.AtomicReference;
|
import org.junit.jupiter.api.Test;
|
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.ListOperations;
|
import org.springframework.data.redis.core.SessionCallback;
|
import org.springframework.data.redis.core.SetOperations;
|
import org.springframework.data.redis.core.ValueOperations;
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONObject;
|
import com.battery.device.api.model.DeviceWorkStatus;
|
import com.battery.device.simulator.BtsDevelopmentSimulator;
|
import com.battery.device.simulator.BtsSimulatorStateStore.ChannelState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.ChannelTombstone;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.DeviceState;
|
import com.battery.device.simulator.BtsSimulatorStateStore.SimulatorState;
|
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
|
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
|
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate;
|
import com.battery.device.api.model.ChannelIdentity;
|
import com.ruoyi.framework.config.FastJson2JsonRedisSerializer;
|
|
class RedisStateStoreTest
|
{
|
@Test
|
void channelParametersAndPlanSurviveRedisRestartAndOnlineChanges() throws Exception
|
{
|
var stored = new AtomicReference<Object>();
|
var configuration = new com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration(
|
new java.math.BigDecimal("-3.123456"), new java.math.BigDecimal("8.125"),
|
new java.math.BigDecimal("7.2"), new java.math.BigDecimal("29.7"), 2, 2, 5, 120, 30,
|
List.of(new com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep("充电", "cc", 60, 4.2),
|
new com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep("放电 <验证>", "dc", 90, 3.0)));
|
try (var simulator = new BtsDevelopmentSimulator(0, store(stored))) {
|
simulator.configureChannel(1, 1, configuration);
|
simulator.updateChannel(1, 1, false, DeviceWorkStatus.PAUSED);
|
simulator.setAllDevicesPoweredOn(false);
|
simulator.setSoftwareAccessible(false);
|
}
|
try (var simulator = new BtsDevelopmentSimulator(0, store(stored))) {
|
assertEquals(configuration, simulator.snapshot().devices().get(0).channels().get(0).configuration());
|
simulator.setSoftwareAccessible(true);
|
simulator.setAllDevicesPoweredOn(true);
|
simulator.updateChannel(1, 1, true, DeviceWorkStatus.WORKING);
|
var identity = simulator.discoverChannels().get(0).identity();
|
assertEquals(-3.123456, simulator.latestCabinet(identity).currentA());
|
assertEquals(8.125, simulator.latestCabinet(identity).voltageV());
|
assertEquals("放电 <验证>", simulator.testPlan(identity).rows().get(1).stepName());
|
assertEquals("进行中", simulator.testPlan(identity).rows().get(1).executionStatus());
|
}
|
}
|
|
@Test
|
void oldChannelStateReceivesDefaultConfigurationWithoutResettingIdentity()
|
{
|
var object = JSON.parseObject(JSON.toJSONString(state(7, "127.0.0.9")));
|
object.getJSONArray("devices").getJSONObject(0).getJSONArray("channels").getJSONObject(0).remove("configuration");
|
var restored = store(new AtomicReference<>(object.toJSONString())).loadOrInitialize(state(1, "127.0.0.2"));
|
assertEquals(7, restored.devices().get(0).deviceId());
|
assertEquals("127.0.0.9", restored.devices().get(0).ip());
|
assertEquals(1, restored.devices().get(0).channels().get(0).configuration().steps().size());
|
}
|
|
@Test
|
void initializesOnlyWhenStateIsAbsentAndRestoresSavedState()
|
{
|
AtomicReference<Object> stored = new AtomicReference<>();
|
RedisBtsSimulatorStateStore store = store(stored);
|
SimulatorState defaults = state(1, "127.0.0.2");
|
|
assertEquals(defaults, store.loadOrInitialize(defaults));
|
SimulatorState changed = state(7, "127.0.0.9");
|
store.save(changed);
|
|
assertEquals(changed, store.loadOrInitialize(defaults));
|
}
|
|
@Test
|
void restoresNestedSimulatorStateFromRedisSerializer()
|
{
|
SimulatorState expected = new SimulatorState(1,
|
List.of(new DeviceState(2, "127.0.0.3", false,
|
List.of(new ChannelState(1, true, DeviceWorkStatus.WORKING)))),
|
List.of(new CollectorState(3, "dev-sim-collector-3", "127.0.1.3",
|
"02:43:00:00:00:03", "模拟采集器", "开发模拟器",
|
"Virtual Collector", 3, true)),
|
3, 4, List.of(new ChannelTombstone(1, "127.0.0.2", 4, 2)), false, 900L);
|
FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
|
AtomicReference<Object> stored = new AtomicReference<>(
|
serializer.deserialize(serializer.serialize(JSON.toJSONString(expected))));
|
|
SimulatorState restored = store(stored).loadOrInitialize(state(1, "127.0.0.2"));
|
|
assertEquals(expected, restored);
|
assertEquals(DeviceWorkStatus.WORKING, restored.devices().get(0).channels().get(0).workStatus());
|
}
|
|
@Test
|
void restoresChannelPlanWhenRedisReturnsNestedJsonObjects()
|
{
|
JSONObject state = JSON.parseObject(JSON.toJSONString(state(1, "127.0.0.2")));
|
JSONObject configuration = state.getJSONArray("devices").getJSONObject(0)
|
.getJSONArray("channels").getJSONObject(0).getJSONObject("configuration");
|
configuration.getJSONArray("steps").forEach(item -> ((JSONObject) item).put("@type", JSONObject.class.getName()));
|
FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
|
Object restoredValue = serializer.deserialize(serializer.serialize(state));
|
|
SimulatorState restored = store(new AtomicReference<>(restoredValue))
|
.loadOrInitialize(state(9, "127.0.0.9"));
|
|
assertEquals("恒流充电", restored.devices().get(0).channels().get(0)
|
.configuration().steps().get(0).name());
|
}
|
|
@Test
|
void upgradesLegacyCollectorDiscoveryStateWithSamplingDefaults()
|
{
|
AtomicReference<Object> stored = new AtomicReference<>("{\"version\":1,\"devices\":[],"
|
+ "\"collectors\":[{\"collectorId\":1,\"deviceCode\":\"dev-sim-collector-1\","
|
+ "\"ip\":\"127.0.1.2\",\"mac\":\"02:43:00:00:00:01\","
|
+ "\"name\":\"模拟采集器\",\"manufacturer\":\"开发模拟器\","
|
+ "\"model\":\"Virtual Collector\",\"channelCount\":2,\"online\":true}],"
|
+ "\"nextDeviceId\":1,\"nextCollectorId\":2,\"tombstones\":[]}");
|
|
SimulatorState restored = store(stored).loadOrInitialize(state(1, "127.0.0.2"));
|
|
CollectorState collector = restored.collectors().get(0);
|
assertEquals(1000, collector.sampleIntervalMs());
|
assertEquals(List.of("S-01", "S-02"),
|
collector.channels().stream().map(channel -> channel.channelCode()).toList());
|
}
|
|
@Test
|
void rejectsIncompatibleVersionWithoutOverwritingIt()
|
{
|
AtomicReference<Object> stored = new AtomicReference<>("{\"version\":2,\"devices\":[],"
|
+ "\"collectors\":[],\"nextDeviceId\":1,\"nextCollectorId\":1,\"tombstones\":[]}");
|
RedisBtsSimulatorStateStore store = store(stored);
|
|
IllegalStateException exception = assertThrows(IllegalStateException.class,
|
() -> store.loadOrInitialize(state(1, "127.0.0.2")));
|
|
assertEquals("Unsupported BTS simulator state version: 2", exception.getMessage());
|
assertEquals(true, stored.get().toString().contains("\"version\":2"));
|
}
|
|
@Test
|
void restoresNestedDiscoveryCandidatesFromJson()
|
{
|
AtomicReference<Object> stored = new AtomicReference<>();
|
RedisDeviceDiscoveryCandidateStore first = new RedisDeviceDiscoveryCandidateStore(redis(stored));
|
java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z");
|
CabinetCandidate candidate = new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1,
|
"127.0.0.2", null, "模拟柜", "开发模拟器", "BTS Virtual Cabinet",
|
List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true,
|
DeviceWorkStatus.WORKING, observedAt)), observedAt, observedAt.plusSeconds(30));
|
first.recordCabinets(List.of(candidate));
|
|
RedisDeviceDiscoveryCandidateStore restored = new RedisDeviceDiscoveryCandidateStore(redis(stored));
|
CabinetCandidate actual = restored.snapshot(observedAt.plusSeconds(1)).cabinets().get(0);
|
|
assertEquals("dev-sim-22-1", actual.deviceCode());
|
assertEquals(DeviceWorkStatus.WORKING, actual.channels().get(0).workStatus());
|
assertEquals(observedAt, actual.channels().get(0).observedAt());
|
}
|
|
@Test
|
void removesDevelopmentCandidatesFromRedisState()
|
{
|
AtomicReference<Object> stored = new AtomicReference<>();
|
RedisDeviceDiscoveryCandidateStore store = new RedisDeviceDiscoveryCandidateStore(redis(stored));
|
java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z");
|
store.recordCabinets(List.of(new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1,
|
"127.0.0.2", null, "模拟柜", null, null,
|
List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true,
|
DeviceWorkStatus.STOPPED, observedAt)), observedAt, observedAt.plusSeconds(30))));
|
|
store.removeAdapter("dev-sim");
|
|
assertEquals(List.of(), store.snapshot(observedAt).cabinets());
|
}
|
|
@Test
|
void restoresDiscoveryCandidatesFromLegacyAutoTypeJson()
|
{
|
AtomicReference<Object> stored = new AtomicReference<>();
|
RedisDeviceDiscoveryCandidateStore first = new RedisDeviceDiscoveryCandidateStore(redis(stored));
|
java.time.Instant observedAt = java.time.Instant.parse("2026-08-27T00:00:00Z");
|
first.recordCabinets(List.of(new CabinetCandidate("dev-sim", null, "dev-sim-22-1", 22, 1,
|
"127.0.0.2", null, "模拟柜", null, null,
|
List.of(new ChannelCandidate(new ChannelIdentity("dev-sim", 22, 1, 1, 1), true,
|
DeviceWorkStatus.STOPPED, observedAt)), observedAt, observedAt.plusSeconds(30))));
|
first.replaceCollectors("dev-sim", List.of(new CollectorCandidate("dev-sim", "dev-sim-collector-1",
|
"127.0.1.2", null, "模拟采集器", null, null, 8, true,
|
observedAt, observedAt.plusSeconds(30))));
|
|
JSONObject legacy = JSON.parseObject(stored.get().toString());
|
legacy.put("@type", "com.battery.business.runtime.RedisDeviceDiscoveryCandidateStore$StoredState");
|
JSONObject cabinet = legacy.getJSONArray("cabinets").getJSONObject(0);
|
cabinet.put("@type", JSONObject.class.getName());
|
cabinet.getJSONArray("channels").getJSONObject(0)
|
.put("@type", JSONObject.class.getName());
|
legacy.getJSONArray("collectors").getJSONObject(0)
|
.put("@type", JSONObject.class.getName());
|
FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
|
stored.set(serializer.deserialize(serializer.serialize(legacy)));
|
|
var snapshot = new RedisDeviceDiscoveryCandidateStore(redis(stored)).snapshot(observedAt.plusSeconds(1));
|
CabinetCandidate restored = snapshot.cabinets().get(0);
|
|
assertEquals("dev-sim-22-1", restored.deviceCode());
|
assertEquals(DeviceWorkStatus.STOPPED, restored.channels().get(0).workStatus());
|
assertEquals("dev-sim-collector-1", snapshot.collectors().get(0).deviceCode());
|
}
|
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
private RedisBtsSimulatorStateStore store(AtomicReference<Object> stored)
|
{
|
return new RedisBtsSimulatorStateStore(redis(stored));
|
}
|
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
private RedisTemplate<Object, Object> redis(AtomicReference<Object> stored)
|
{
|
RedisTemplate<Object, Object> redis = mock(RedisTemplate.class);
|
ValueOperations<Object, Object> values = mock(ValueOperations.class);
|
ListOperations<Object, Object> lists = mock(ListOperations.class);
|
SetOperations<Object, Object> sets = mock(SetOperations.class);
|
Map<Object, List<Object>> listValues = new LinkedHashMap<>();
|
when(redis.opsForValue()).thenReturn(values);
|
when(redis.opsForList()).thenReturn(lists);
|
when(redis.opsForSet()).thenReturn(sets);
|
when(sets.add(any(), any())).thenReturn(1L);
|
when(sets.members(any())).thenReturn(Set.of());
|
when(redis.exec()).thenReturn(List.of(Boolean.TRUE));
|
when(redis.execute(any(SessionCallback.class))).thenAnswer(invocation -> {
|
SessionCallback<?> callback = invocation.getArgument(0);
|
return callback.execute(redis);
|
});
|
when(lists.rightPushAll(any(), org.mockito.ArgumentMatchers.<Object>anyCollection()))
|
.thenAnswer(invocation -> {
|
Object key = invocation.getArgument(0);
|
List<Object> records = new ArrayList<>(listValues.getOrDefault(key, List.of()));
|
FastJson2JsonRedisSerializer<Object> serializer =
|
new FastJson2JsonRedisSerializer<>(Object.class);
|
List<Object> appended = invocation.getArgument(1);
|
appended.forEach(item -> records.add(
|
serializer.deserialize(serializer.serialize(item))));
|
listValues.put(key, records);
|
return (long) records.size();
|
});
|
when(lists.range(any(), anyLong(), anyLong())).thenAnswer(invocation -> {
|
List<Object> records = listValues.getOrDefault(invocation.getArgument(0), List.of());
|
long requestedStart = invocation.getArgument(1);
|
long requestedEnd = invocation.getArgument(2);
|
int start = (int) Math.min(requestedStart, records.size());
|
int end = (int) Math.min(requestedEnd + 1, records.size());
|
return start >= end ? List.of() : List.copyOf(records.subList(start, end));
|
});
|
when(lists.size(any())).thenAnswer(invocation ->
|
(long) listValues.getOrDefault(invocation.getArgument(0), List.of()).size());
|
when(redis.delete(org.mockito.ArgumentMatchers.<Object>any()))
|
.thenAnswer(invocation -> listValues.remove(invocation.getArgument(0)) != null);
|
when(values.get(any())).thenAnswer(invocation -> stored.get());
|
when(values.setIfAbsent(any(), any())).thenAnswer(invocation -> {
|
if (stored.get() != null) return false;
|
stored.set(invocation.getArgument(1));
|
return true;
|
});
|
doAnswer(invocation -> { stored.set(invocation.getArgument(1)); return null; })
|
.when(values).set(any(), any());
|
return redis;
|
}
|
|
private SimulatorState state(int deviceId, String ip)
|
{
|
return new SimulatorState(1,
|
List.of(new DeviceState(deviceId, ip, true,
|
List.of(new ChannelState(1, true, DeviceWorkStatus.STOPPED)))),
|
List.of(), deviceId + 1, 1, List.of());
|
}
|
}
|