| | |
| | | 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.mock; |
| | | 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; |
| | |
| | | assertEquals(1L, collector.nextSequence()); |
| | | assertEquals(List.of("S-01", "S-02"), |
| | | collector.channels().stream().map(channel -> channel.channelCode()).toList()); |
| | | assertEquals(List.of(), collector.history()); |
| | | 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.loadCollectorHistory("dev-sim-collector-1").size()); |
| | | assertEquals(true, stored.get().toString().contains("\"history\":[{")); |
| | | store.completeValidatedRestore(restored); |
| | | assertEquals(false, stored.get().toString().contains("\"history\":[{")); |
| | | } |
| | | |
| | | @Test |
| | | void doesNotOverwriteDamagedEmbeddedHistoryBeforeValidation() |
| | | { |
| | | 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\":[]}"); |
| | | |
| | | assertThrows(IllegalStateException.class, |
| | | () -> new BtsDevelopmentSimulator(0, store(stored))); |
| | | |
| | | assertEquals(true, stored.get().toString().contains("\"history\":[{")); |
| | | } |
| | | |
| | | @Test |
| | |
| | | |
| | | 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.loadCollectorHistory("dev-sim-collector-1")); |
| | | } |
| | | |
| | | @Test |
| | |
| | | { |
| | | 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.copyOf(listValues.getOrDefault(invocation.getArgument(0), List.of()))); |
| | | 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; |