yeg
2026-09-02 29f1afa5fff5440ad5ce7dd40dee1053ea0ff7bf
ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java
@@ -3,17 +3,28 @@
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.ruoyi.device.model.DeviceWorkStatus;
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelState;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelTombstone;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState;
@@ -62,6 +73,86 @@
    }
    @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(1L, collector.nextSequence());
        assertEquals(List.of("S-01", "S-02"),
                collector.channels().stream().map(channel -> channel.channelCode()).toList());
        assertEquals(0L, collector.historyRecordCount());
    }
    @Test
    void migratesEmbeddedCollectorHistoryToAppendOnlyRedisList()
    {
        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\":1,\"online\":true,"
                + "\"sampleIntervalMs\":1000,\"nextSequence\":2,"
                + "\"lastSampledAt\":\"2026-08-29T05:00:00Z\","
                + "\"channels\":[{\"channelCode\":\"S-01\",\"metrics\":[{"
                + "\"metricCode\":\"temperature\",\"value\":25.1,\"unit\":\"℃\"}]}],"
                + "\"history\":[{\"adapterCode\":\"dev-sim\","
                + "\"collectorCode\":\"dev-sim-collector-1\",\"channelCode\":\"S-01\","
                + "\"sequenceId\":1,\"deviceTime\":\"2026-08-29T05:00:00Z\","
                + "\"deviceTimeRaw\":\"2026-08-29 13:00:00.000\","
                + "\"serverReceivedAt\":\"2026-08-29T05:00:00Z\","
                + "\"metricCode\":\"temperature\",\"value\":25.1,\"unit\":\"℃\"}],"
                + "\"historyFaults\":{\"missingSequences\":[],\"outOfOrder\":false}}],"
                + "\"nextDeviceId\":1,\"nextCollectorId\":2,\"tombstones\":[]}");
        RedisBtsSimulatorStateStore store = store(stored);
        SimulatorState restored = store.loadOrInitialize(state(1, "127.0.0.2"));
        assertEquals(1L, restored.collectors().get(0).historyRecordCount());
        assertEquals(1, store.loadCollectorHistoryPage("dev-sim-collector-1", 0, 1000, false)
                .records().size());
        assertEquals(true, stored.get().toString().contains("\"history\":[{"));
        store.completeValidatedRestore(restored);
        assertEquals(false, stored.get().toString().contains("\"history\":[{"));
    }
    @Test
    void clearsLegacyEmbeddedHistoryForDemoStartup() throws Exception
    {
        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\":1,\"online\":true,"
                + "\"sampleIntervalMs\":1000,\"nextSequence\":2,"
                + "\"lastSampledAt\":\"2026-08-29T05:00:00Z\","
                + "\"channels\":[{\"channelCode\":\"S-01\",\"metrics\":[{"
                + "\"metricCode\":\"temperature\",\"value\":25.1,\"unit\":\"℃\"}]}],"
                + "\"history\":[{\"adapterCode\":\"dev-sim\","
                + "\"collectorCode\":\"wrong-collector\",\"channelCode\":\"S-01\","
                + "\"sequenceId\":1,\"deviceTime\":\"2026-08-29T05:00:00Z\","
                + "\"deviceTimeRaw\":\"2026-08-29 13:00:00.000\","
                + "\"serverReceivedAt\":\"2026-08-29T05:00:00Z\","
                + "\"metricCode\":\"temperature\",\"value\":25.1,\"unit\":\"℃\"}],"
                + "\"historyFaults\":{\"missingSequences\":[],\"outOfOrder\":false}}],"
                + "\"nextDeviceId\":1,\"nextCollectorId\":2,\"tombstones\":[]}");
        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store(stored)))
        {
            assertEquals(0L, simulator.snapshot().collectors().get(0).historyRecordCount());
        }
        assertEquals(false, stored.get().toString().contains("\"history\":[{"));
    }
    @Test
    void rejectsIncompatibleVersionWithoutOverwritingIt()
    {
        AtomicReference<Object> stored = new AtomicReference<>("{\"version\":2,\"devices\":[],"
@@ -73,6 +164,58 @@
        assertEquals("Unsupported BTS simulator state version: 2", exception.getMessage());
        assertEquals(true, stored.get().toString().contains("\"version\":2"));
    }
    @Test
    void atomicallySavesCollectorDeletionAndRemovesIndependentHistory()
    {
        AtomicReference<Object> stored = new AtomicReference<>();
        RedisBtsSimulatorStateStore store = store(stored);
        SimulatorState current = state(1, "127.0.0.2");
        store.loadOrInitialize(current);
        var record = new com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord(
                "dev-sim", "dev-sim-collector-1", "S-01", 1L,
                java.time.Instant.parse("2026-08-29T05:00:00Z"), "2026-08-29 13:00:00.000",
                java.time.Instant.parse("2026-08-29T05:00:00Z"), "temperature",
                new java.math.BigDecimal("25.1"), "℃");
        store.save(current, List.of(
                new com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend(
                        "dev-sim-collector-1", List.of(record))));
        SimulatorState next = state(7, "127.0.0.9");
        store.saveAndDeleteCollectorHistory(next, "dev-sim-collector-1");
        assertEquals(next, store.loadOrInitialize(current));
        assertEquals(List.of(), store.loadCollectorHistoryPage("dev-sim-collector-1", 0, 1000, false)
                .records());
    }
    @Test
    void readsOnlyRequestedCollectorHistoryRange()
    {
        AtomicReference<Object> stored = new AtomicReference<>();
        RedisTemplate<Object, Object> redis = redis(stored);
        RedisBtsSimulatorStateStore store = new RedisBtsSimulatorStateStore(redis);
        SimulatorState current = state(1, "127.0.0.2");
        store.loadOrInitialize(current);
        var record = new com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord(
                "dev-sim", "dev-sim-collector-1", "S-01", 1L,
                java.time.Instant.parse("2026-08-29T05:00:00Z"), "2026-08-29 13:00:00.000",
                java.time.Instant.parse("2026-08-29T05:00:00Z"), "temperature",
                new java.math.BigDecimal("25.1"), "℃");
        store.save(current, List.of(
                new com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend(
                        "dev-sim-collector-1", java.util.Collections.nCopies(1010, record))));
        ListOperations<Object, Object> lists = redis.opsForList();
        clearInvocations(lists);
        var page = store.loadCollectorHistoryPage("dev-sim-collector-1", 1000, 10, false);
        assertEquals(1010, page.total());
        assertEquals(10, page.records().size());
        assertEquals(false, page.hasMore());
        verify(lists).range(RedisBtsSimulatorStateStore.HISTORY_KEY_PREFIX + "dev-sim-collector-1",
                1000L, 1009L);
    }
    @Test
@@ -155,7 +298,43 @@
    {
        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;