2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.network.internal;
15 import static org.hamcrest.CoreMatchers.is;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.*;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.Mockito.*;
21 import java.io.IOException;
22 import java.net.UnknownHostException;
23 import java.util.Collections;
24 import java.util.Optional;
25 import java.util.concurrent.ExecutorService;
26 import java.util.concurrent.TimeUnit;
27 import java.util.function.Consumer;
29 import org.junit.jupiter.api.AfterEach;
30 import org.junit.jupiter.api.BeforeEach;
31 import org.junit.jupiter.api.Test;
32 import org.junit.jupiter.api.extension.ExtendWith;
33 import org.mockito.ArgumentCaptor;
34 import org.mockito.Mock;
35 import org.mockito.junit.jupiter.MockitoExtension;
36 import org.mockito.junit.jupiter.MockitoSettings;
37 import org.mockito.quality.Strictness;
38 import org.openhab.binding.network.internal.toberemoved.cache.ExpiringCacheAsync;
39 import org.openhab.binding.network.internal.toberemoved.cache.ExpiringCacheHelper;
40 import org.openhab.binding.network.internal.utils.NetworkUtils;
41 import org.openhab.binding.network.internal.utils.NetworkUtils.ArpPingUtilEnum;
42 import org.openhab.binding.network.internal.utils.NetworkUtils.IpPingMethodEnum;
43 import org.openhab.binding.network.internal.utils.PingResult;
46 * Tests cases for {@see PresenceDetectionValue}
48 * @author David Graeff - Initial contribution
50 @ExtendWith(MockitoExtension.class)
51 @MockitoSettings(strictness = Strictness.LENIENT)
52 public class PresenceDetectionTest {
53 private static final long CACHETIME = 2000L;
55 private PresenceDetection subject;
57 private @Mock Consumer<PresenceDetectionValue> callback;
58 private @Mock ExecutorService executorService;
59 private @Mock PresenceDetectionListener listener;
60 private @Mock NetworkUtils networkUtils;
63 public void setUp() throws UnknownHostException {
65 when(networkUtils.getInterfaceNames()).thenReturn(Collections.singleton("TESTinterface"));
66 doReturn(ArpPingUtilEnum.IPUTILS_ARPING).when(networkUtils).determineNativeARPpingMethod(anyString());
67 doReturn(IpPingMethodEnum.WINDOWS_PING).when(networkUtils).determinePingMethod();
69 subject = spy(new PresenceDetection(listener, (int) CACHETIME));
70 subject.networkUtils = networkUtils;
71 subject.cache = spy(new ExpiringCacheAsync<>(CACHETIME, () -> {
72 subject.performPresenceDetection(false);
75 // Set a useful configuration. The default presenceDetection is a no-op.
76 subject.setHostname("127.0.0.1");
77 subject.setTimeout(300);
78 subject.setUseDhcpSniffing(false);
79 subject.setIOSDevice(true);
80 subject.setServicePorts(Collections.singleton(1010));
81 subject.setUseArpPing(true, "arping", ArpPingUtilEnum.IPUTILS_ARPING);
82 subject.setUseIcmpPing(true);
84 assertThat(subject.pingMethod, is(IpPingMethodEnum.WINDOWS_PING));
88 public void shutDown() {
89 subject.waitForPresenceDetection();
92 // Depending on the amount of test methods an according amount of threads is spawned.
93 // We will check if they spawn and return in time.
95 public void threadCountTest() {
96 assertNull(subject.executorService);
98 doNothing().when(subject).performARPping(any());
99 doNothing().when(subject).performJavaPing();
100 doNothing().when(subject).performSystemPing();
101 doNothing().when(subject).performServicePing(anyInt());
103 subject.performPresenceDetection(false);
105 // Thread count: ARP + ICMP + 1*TCP
106 assertThat(subject.detectionChecks, is(3));
107 assertNotNull(subject.executorService);
109 subject.waitForPresenceDetection();
110 assertThat(subject.detectionChecks, is(0));
111 assertNull(subject.executorService);
115 public void partialAndFinalCallbackTests() throws InterruptedException, IOException {
116 doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING),
117 anyString(), anyInt());
118 doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils)
119 .nativeARPPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(), anyString(), any(), anyInt());
120 doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).servicePing(anyString(), anyInt(), anyInt());
122 assertTrue(subject.performPresenceDetection(false));
123 subject.waitForPresenceDetection();
125 verify(subject, times(0)).performJavaPing();
126 verify(subject).performSystemPing();
127 verify(subject).performARPping(any());
128 verify(subject).performServicePing(anyInt());
130 verify(listener, times(3)).partialDetectionResult(any());
131 ArgumentCaptor<PresenceDetectionValue> capture = ArgumentCaptor.forClass(PresenceDetectionValue.class);
132 verify(listener, times(1)).finalDetectionResult(capture.capture());
134 assertThat(capture.getValue().getSuccessfulDetectionTypes(), is("ARP_PING, ICMP_PING, TCP_CONNECTION"));
138 public void cacheTest() throws InterruptedException, IOException {
139 doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING),
140 anyString(), anyInt());
141 doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils)
142 .nativeARPPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(), anyString(), any(), anyInt());
143 doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).servicePing(anyString(), anyInt(), anyInt());
145 doReturn(executorService).when(subject).getThreadsFor(anyInt());
147 // We expect no valid value
148 assertTrue(subject.cache.isExpired());
149 // Get value will issue a PresenceDetection internally.
150 subject.getValue(callback);
151 verify(subject).performPresenceDetection(eq(false));
152 assertNotNull(subject.executorService);
153 // There should be no straight callback yet
154 verify(callback, times(0)).accept(any());
156 // Perform the different presence detection threads now
157 ArgumentCaptor<Runnable> capture = ArgumentCaptor.forClass(Runnable.class);
158 verify(executorService, times(3)).execute(capture.capture());
159 for (Runnable r : capture.getAllValues()) {
162 // "Wait" for the presence detection to finish
163 subject.waitForPresenceDetection();
165 // Although there are multiple partial results and a final result,
166 // the getValue() consumers get the fastest response possible, and only once.
167 verify(callback, times(1)).accept(any());
169 // As long as the cache is valid, we can get the result back again
170 subject.getValue(callback);
171 verify(callback, times(2)).accept(any());
173 // Invalidate value, we should not get a new callback immediately again
174 subject.cache.invalidateValue();
175 subject.getValue(callback);
176 verify(callback, times(2)).accept(any());
180 public void reuseValueTests() throws InterruptedException, IOException {
181 final long startTime = 1000L;
182 when(subject.cache.getCurrentNanoTime()).thenReturn(TimeUnit.MILLISECONDS.toNanos(startTime));
184 // The PresenceDetectionValue.getLowestLatency() should return the smallest latency
185 PresenceDetectionValue v = subject.updateReachableValue(PresenceDetectionType.ICMP_PING, 20);
186 PresenceDetectionValue v2 = subject.updateReachableValue(PresenceDetectionType.ICMP_PING, 19);
188 assertThat(v.getLowestLatency(), is(19.0));
190 // Advance in time but not expire the cache (1ms left)
191 final long almostExpire = startTime + CACHETIME - 1;
192 when(subject.cache.getCurrentNanoTime()).thenReturn(TimeUnit.MILLISECONDS.toNanos(almostExpire));
194 // Updating should reset the expire timer of the cache
195 v2 = subject.updateReachableValue(PresenceDetectionType.ICMP_PING, 28);
197 assertThat(v2.getLowestLatency(), is(19.0));
198 assertThat(ExpiringCacheHelper.expireTime(subject.cache),
199 is(TimeUnit.MILLISECONDS.toNanos(almostExpire + CACHETIME)));
201 // Cache expire. A new PresenceDetectionValue instance will be returned
202 when(subject.cache.getCurrentNanoTime())
203 .thenReturn(TimeUnit.MILLISECONDS.toNanos(almostExpire + CACHETIME + CACHETIME + 1));
204 v2 = subject.updateReachableValue(PresenceDetectionType.ICMP_PING, 25);
205 assertNotEquals(v, v2);
206 assertThat(v2.getLowestLatency(), is(25.0));