yeg
6 天以前 e38aca8134570513435c003c25dc05f1ae51c9c9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package com.battery.business.service.impl;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import com.battery.device.api.DeviceAdapter;
import com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore;
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.model.DetectedChannel;
import com.battery.device.api.registry.DeviceAdapterRegistry;
import com.battery.business.mapper.TDetectionCabinetMapper;
import com.battery.business.mapper.TTestMapper;
 
class DeviceDiscoveryPersistenceServiceTest
{
    /**
     * 来源不可达时,既有候选保留来源身份但旧快照被撤下、在线转为离线,登记设备同步离线;
     * 来源恢复后下一次成功发现重新带回实时事实。
     */
    @Test
    void failedDiscoveryInvalidatesSuccessfulSnapshotUntilSourceReturns()
    {
        InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
        TDetectionCabinetMapper cabinetMapper = Mockito.mock(TDetectionCabinetMapper.class);
        DeviceAdapterRegistry registry = Mockito.mock(DeviceAdapterRegistry.class);
        DeviceAdapter adapter = Mockito.mock(DeviceAdapter.class);
        ChannelIdentity identity = new ChannelIdentity("neware76", 21, 36, 1, 2);
        java.time.Instant observedAt = java.time.Instant.parse("2026-09-28T01:54:56Z");
        com.battery.device.api.model.RealtimeSnapshot snapshot = new com.battery.device.api.model.RealtimeSnapshot(
                identity, com.battery.device.api.model.DeviceWorkStatus.WORKING, 3, 7, "1", null,
                new java.math.BigDecimal("1.2"), new java.math.BigDecimal("3.3"),
                new java.math.BigDecimal("0.5"), new java.math.BigDecimal("1.1"), 60L, 30L, null, observedAt, java.util.Map.of());
        when(adapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "26.1.2.3", true)));
        when(adapter.inquire(any())).thenReturn(java.util.Map.of(identity, snapshot));
        when(registry.execute(eq("neware76"), any())).thenAnswer(invocation -> {
            @SuppressWarnings("unchecked")
            Function<DeviceAdapter, Object> operation = (Function<DeviceAdapter, Object>) invocation.getArgument(1);
            return operation.apply(adapter);
        });
        DeviceDiscoveryPersistenceService service = new DeviceDiscoveryPersistenceService(store, cabinetMapper);
        service.discoverForReception(registry, new DeviceDiscoveryPersistenceService.DiscoverySource(
                "neware76", 8L, "新威 7.6 那台", 60000, true));
 
        var before = store.snapshot(java.time.Instant.now()).cabinets().get(0);
        org.junit.jupiter.api.Assertions.assertNotNull(before.channels().get(0).latestSnapshot());
        org.junit.jupiter.api.Assertions.assertTrue(before.online());
 
        service.markUnavailable("neware76");
 
        var failed = store.snapshot(java.time.Instant.now()).cabinets().get(0);
        assertEquals(before.deviceCode(), failed.deviceCode());
        org.junit.jupiter.api.Assertions.assertFalse(failed.online());
        org.junit.jupiter.api.Assertions.assertNull(failed.channels().get(0).latestSnapshot());
        verify(cabinetMapper).markRegisteredCabinetsOffline("neware76");
    }
 
    @Test
    void discoveryPersistsCandidatesWithoutReceivingTest()
    {
        InMemoryDeviceDiscoveryCandidateStore candidateStore = new InMemoryDeviceDiscoveryCandidateStore();
        TDetectionCabinetMapper cabinetMapper = Mockito.mock(TDetectionCabinetMapper.class);
        TTestMapper testMapper = Mockito.mock(TTestMapper.class);
        DeviceAdapterRegistry registry = Mockito.mock(DeviceAdapterRegistry.class);
        DeviceAdapter adapter = Mockito.mock(DeviceAdapter.class);
        ChannelIdentity identity = new ChannelIdentity("lab-bts", 22, 1, 1, 1);
        when(testMapper.hasReceivingTestForAdapter("lab-bts")).thenReturn(0);
        when(adapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "127.0.0.1", true)));
        when(registry.execute(eq("lab-bts"), any())).thenAnswer(invocation -> {
            @SuppressWarnings("unchecked")
            Function<DeviceAdapter, Object> operation = (Function<DeviceAdapter, Object>) invocation.getArgument(1);
            return operation.apply(adapter);
        });
 
        DeviceDiscoveryPersistenceService service = new DeviceDiscoveryPersistenceService(candidateStore,
                cabinetMapper, testMapper);
 
        var result = service.discoverForReception(registry,
                new DeviceDiscoveryPersistenceService.DiscoverySource("lab-bts", 7L, "一号 BTS", 1000));
 
        assertEquals(1, result.cabinetCount());
        assertEquals(1, candidateStore.snapshot(java.time.Instant.now()).cabinets().size());
        verify(adapter).discoverChannels();
    }
 
    /**
     * 直连厂商存储的来源:通道登记是长期事实,候选不按时间过期,
     * 每条候选附带最近一次采样行的设备时间,供操作员判断哪几台真的在产出数据。
     */
    @Test
    void vendorSourceCandidatesCarryLastDataTimeAndDoNotExpireByTime()
    {
        InMemoryDeviceDiscoveryCandidateStore candidateStore = new InMemoryDeviceDiscoveryCandidateStore();
        TDetectionCabinetMapper cabinetMapper = Mockito.mock(TDetectionCabinetMapper.class);
        TTestMapper testMapper = Mockito.mock(TTestMapper.class);
        DeviceAdapterRegistry registry = Mockito.mock(DeviceAdapterRegistry.class);
        DeviceAdapter adapter = Mockito.mock(DeviceAdapter.class);
        ChannelIdentity identity = new ChannelIdentity("neware76", 21, 36, 1, 2);
        java.time.Instant lastDataAt = java.time.Instant.parse("2026-09-28T01:54:56Z");
        when(testMapper.hasReceivingTestForAdapter("neware76")).thenReturn(0);
        when(adapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "26.1.2.3", true,
                com.battery.device.api.model.DetectedDeviceMetadata.empty(), lastDataAt)));
        when(registry.execute(eq("neware76"), any())).thenAnswer(invocation -> {
            @SuppressWarnings("unchecked")
            Function<DeviceAdapter, Object> operation = (Function<DeviceAdapter, Object>) invocation.getArgument(1);
            return operation.apply(adapter);
        });
 
        DeviceDiscoveryPersistenceService service = new DeviceDiscoveryPersistenceService(candidateStore,
                cabinetMapper, testMapper);
        service.discoverForReception(registry, new DeviceDiscoveryPersistenceService.DiscoverySource(
                "neware76", 8L, "新威 7.6 那台", 60000, true));
 
        var candidate = candidateStore.snapshot(java.time.Instant.now()).cabinets().get(0);
        assertEquals(lastDataAt, candidate.lastDataAt());
        assertEquals(lastDataAt, candidate.channels().get(0).lastDataAt());
        // 一年后仍然是候选:候选只按"已登记且未删除"排除,不按时间过期。
        assertEquals(1, candidateStore.snapshot(java.time.Instant.now().plusSeconds(365L * 24 * 3600))
                .cabinets().size());
    }
}