Skip to content

Commit 2e64b55

Browse files
authored
[smartmeter] Fix test (#21411)
Signed-off-by: Leo Siepel <leosiepel@gmail.com>
1 parent 9b940b7 commit 2e64b55

3 files changed

Lines changed: 237 additions & 38 deletions

File tree

bundles/org.openhab.binding.smartmeter/src/test/java/org/openhab/binding/smartmeter/MockMeterReaderConnector.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
@NonNullByDefault
2828
public class MockMeterReaderConnector extends ConnectorBase<Object> {
2929

30-
private boolean applyRetry;
31-
private Supplier<Object> readNextSupplier;
30+
private final boolean applyRetry;
31+
private final Supplier<Object> readNextSupplier;
3232

3333
protected MockMeterReaderConnector(String portName, boolean applyRetry, Supplier<Object> readNextSupplier) {
3434
super(portName);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
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
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
14+
package org.openhab.binding.smartmeter;
15+
16+
import java.time.Duration;
17+
import java.util.Objects;
18+
import java.util.concurrent.Callable;
19+
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.ScheduledFuture;
21+
import java.util.concurrent.ScheduledThreadPoolExecutor;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.atomic.AtomicBoolean;
24+
25+
import org.eclipse.jdt.annotation.NonNullByDefault;
26+
import org.eclipse.jdt.annotation.Nullable;
27+
28+
/**
29+
*
30+
* @author Leo Siepel - Initial contribution
31+
*
32+
*/
33+
@NonNullByDefault
34+
public class RetrySuppressingExecutor extends ScheduledThreadPoolExecutor {
35+
36+
private final Duration retryDelay;
37+
private final AtomicBoolean suppressNextRetry = new AtomicBoolean();
38+
private final CountDownLatch retrySuppressed = new CountDownLatch(1);
39+
private final CountDownLatch sourceTaskFinished = new CountDownLatch(1);
40+
private @Nullable Thread sourceThread;
41+
42+
RetrySuppressingExecutor(int corePoolSize, Duration retryDelay) {
43+
super(corePoolSize);
44+
this.retryDelay = retryDelay;
45+
}
46+
47+
void suppressNextRetry() {
48+
suppressNextRetry.set(true);
49+
}
50+
51+
void awaitRetrySuppressed(Duration timeout) {
52+
await(retrySuppressed, timeout, "The outer retry was not suppressed");
53+
}
54+
55+
void markSourceTask() {
56+
sourceThread = Thread.currentThread();
57+
}
58+
59+
void awaitSourceTaskFinished(Duration timeout) {
60+
await(sourceTaskFinished, timeout, "The meter source task did not finish");
61+
}
62+
63+
private void await(CountDownLatch latch, Duration timeout, String failureMessage) {
64+
try {
65+
if (!latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
66+
throw new AssertionError(failureMessage);
67+
}
68+
} catch (InterruptedException e) {
69+
Thread.currentThread().interrupt();
70+
throw new AssertionError("Interrupted while waiting for an executor event", e);
71+
}
72+
}
73+
74+
@Override
75+
@NonNullByDefault({})
76+
protected void afterExecute(Runnable runnable, Throwable throwable) {
77+
super.afterExecute(runnable, throwable);
78+
if (Thread.currentThread().equals(sourceThread)) {
79+
sourceTaskFinished.countDown();
80+
}
81+
}
82+
83+
@Override
84+
public <V> ScheduledFuture<V> schedule(@Nullable Callable<V> task, long delay, @Nullable TimeUnit unit) {
85+
Callable<V> callable = Objects.requireNonNull(task);
86+
TimeUnit timeUnit = Objects.requireNonNull(unit);
87+
if (timeUnit.toMillis(delay) == retryDelay.toMillis() && suppressNextRetry.compareAndSet(true, false)) {
88+
ScheduledFuture<V> future = super.schedule(() -> null, delay, timeUnit);
89+
retrySuppressed.countDown();
90+
return future;
91+
}
92+
return super.schedule(callable, delay, timeUnit);
93+
}
94+
}

bundles/org.openhab.binding.smartmeter/src/test/java/org/openhab/binding/smartmeter/TestMeterReading.java

Lines changed: 141 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,25 @@
1212
*/
1313
package org.openhab.binding.smartmeter;
1414

15+
import static org.junit.jupiter.api.Assertions.assertTrue;
1516
import static org.mockito.ArgumentMatchers.any;
17+
import static org.mockito.ArgumentMatchers.anyInt;
1618
import static org.mockito.Mockito.*;
1719

1820
import java.io.IOException;
21+
import java.io.UncheckedIOException;
1922
import java.time.Duration;
20-
import java.util.concurrent.Executors;
23+
import java.util.concurrent.CountDownLatch;
24+
import java.util.concurrent.ScheduledExecutorService;
25+
import java.util.concurrent.TimeUnit;
2126
import java.util.concurrent.TimeoutException;
2227
import java.util.function.Supplier;
2328

2429
import javax.measure.Quantity;
2530

2631
import org.eclipse.jdt.annotation.NonNullByDefault;
32+
import org.junit.jupiter.api.AfterEach;
2733
import org.junit.jupiter.api.Test;
28-
import org.mockito.ArgumentMatchers;
2934
import org.mockito.Mockito;
3035
import org.openhab.binding.smartmeter.connectors.ConnectorBase;
3136
import org.openhab.binding.smartmeter.connectors.IMeterReaderConnector;
@@ -47,90 +52,154 @@
4752
@NonNullByDefault
4853
public class TestMeterReading {
4954

55+
private static final Duration OUTER_RETRY_DELAY = Duration.ofSeconds(2);
56+
57+
private RetrySuppressingExecutor createRetrySuppressingExecutor(int threadCount) {
58+
return new RetrySuppressingExecutor(threadCount, OUTER_RETRY_DELAY);
59+
}
60+
61+
private static final Duration EVENT_TIMEOUT = Duration.ofSeconds(10);
62+
private static final Duration READ_PERIOD = Duration.ofMillis(100);
63+
64+
@AfterEach
65+
public void resetRxJavaPlugins() {
66+
RxJavaPlugins.reset();
67+
}
68+
5069
@Test
51-
public void testContinousReading() throws Exception {
52-
final Duration period = Duration.ofSeconds(1);
70+
public void testContinuousReading() {
5371
final int executionCount = 5;
5472
MockMeterReaderConnector connector = getMockedConnector(false, () -> new Object());
5573
MeterDevice<Object> meter = getMeterDevice(connector);
5674
MeterValueListener changeListener = Mockito.mock(MeterValueListener.class);
75+
CountDownLatch valuesChanged = new CountDownLatch(executionCount);
76+
RetrySuppressingExecutor executorService = createRetrySuppressingExecutor(1);
77+
78+
doAnswer(invocation -> {
79+
valuesChanged.countDown();
80+
return null;
81+
}).when(changeListener).valueChanged(any());
5782
meter.addValueChangeListener(changeListener);
58-
long executionTime = period.toMillis() * executionCount;
59-
Disposable disposable = meter.readValues(executionTime, Executors.newScheduledThreadPool(1), period);
83+
84+
Disposable disposable = meter.readValues(EVENT_TIMEOUT.toMillis(), executorService, READ_PERIOD);
85+
6086
try {
61-
verify(changeListener, after(executionTime + period.toMillis() / 2 + 50).never()).errorOccurred(any());
62-
verify(changeListener, times(executionCount)).valueChanged(any());
87+
await(valuesChanged, "Did not receive the expected meter values");
88+
executorService.suppressNextRetry();
6389
} finally {
64-
disposable.dispose();
90+
dispose(disposable, executorService);
6591
}
92+
93+
verify(changeListener, atLeast(executionCount)).valueChanged(any());
94+
verify(changeListener, never()).errorOccurred(any());
6695
}
6796

6897
@Test
6998
public void testRetryHandling() {
70-
final Duration period = Duration.ofSeconds(1);
7199
MockMeterReaderConnector connector = spy(getMockedConnector(true, () -> {
72100
throw new IllegalArgumentException();
73101
}));
74102
MeterDevice<Object> meter = getMeterDevice(connector);
75103
MeterValueListener changeListener = Mockito.mock(MeterValueListener.class);
104+
CountDownLatch readingError = new CountDownLatch(1);
105+
RetrySuppressingExecutor executorService = createRetrySuppressingExecutor(1);
106+
107+
doAnswer(invocation -> {
108+
executorService.suppressNextRetry();
109+
readingError.countDown();
110+
return null;
111+
}).when(changeListener).errorOccurred(any());
76112
meter.addValueChangeListener(changeListener);
77-
Disposable disposable = meter.readValues(5000, Executors.newScheduledThreadPool(1), period);
113+
114+
Disposable disposable = meter.readValues(EVENT_TIMEOUT.toMillis(), executorService, READ_PERIOD);
115+
78116
try {
79-
verify(changeListener, after(
80-
period.toMillis() + 2 * period.toMillis() * ConnectorBase.NUMBER_OF_RETRIES + period.toMillis() / 2)
81-
.times(1)).errorOccurred(any());
82-
verify(connector, times(ConnectorBase.NUMBER_OF_RETRIES)).retryHook(ArgumentMatchers.anyInt());
117+
await(readingError, "Did not receive the expected reading error");
118+
executorService.awaitRetrySuppressed(EVENT_TIMEOUT);
83119
} finally {
84-
disposable.dispose();
120+
dispose(disposable, executorService);
85121
}
122+
123+
verify(changeListener, times(1)).errorOccurred(any());
124+
verify(connector, times(ConnectorBase.NUMBER_OF_RETRIES)).retryHook(anyInt());
86125
}
87126

88127
@Test
89128
public void testTimeoutHandling() {
90-
final Duration period = Duration.ofSeconds(2);
91-
final int timeout = 5000;
129+
final int timeout = 1000;
130+
CountDownLatch readStarted = new CountDownLatch(1);
131+
CountDownLatch releaseRead = new CountDownLatch(1);
92132
MockMeterReaderConnector connector = spy(getMockedConnector(true, () -> {
93-
try {
94-
Thread.sleep(timeout);
95-
} catch (InterruptedException e) {
96-
}
133+
readStarted.countDown();
134+
awaitUninterruptibly(releaseRead);
97135
return new Object();
98136
}));
99137
MeterDevice<Object> meter = getMeterDevice(connector);
100138
MeterValueListener changeListener = Mockito.mock(MeterValueListener.class);
139+
CountDownLatch timeoutOccurred = new CountDownLatch(1);
140+
RetrySuppressingExecutor executorService = createRetrySuppressingExecutor(2);
141+
142+
doAnswer(invocation -> {
143+
executorService.suppressNextRetry();
144+
timeoutOccurred.countDown();
145+
return null;
146+
}).when(changeListener).errorOccurred(any());
101147
meter.addValueChangeListener(changeListener);
102-
Disposable disposable = meter.readValues(timeout / 2, Executors.newScheduledThreadPool(2), period);
148+
149+
Disposable disposable = meter.readValues(timeout, executorService, Duration.ZERO);
150+
103151
try {
104-
verify(changeListener, timeout(timeout)).errorOccurred(any(TimeoutException.class));
152+
await(readStarted, "The meter read did not start");
153+
await(timeoutOccurred, "The meter read did not time out");
154+
executorService.awaitRetrySuppressed(EVENT_TIMEOUT);
105155
} finally {
106-
disposable.dispose();
156+
releaseRead.countDown();
157+
dispose(disposable, executorService);
107158
}
159+
160+
verify(changeListener, times(1)).errorOccurred(any(TimeoutException.class));
108161
}
109162

110163
@Test
111164
public void shouldNotReportToFallbackException() {
112-
final Duration period = Duration.ofSeconds(2);
113-
final int timeout = 5000;
165+
final int timeout = 1000;
166+
CountDownLatch readStarted = new CountDownLatch(1);
167+
CountDownLatch releaseRead = new CountDownLatch(1);
168+
RetrySuppressingExecutor executorService = createRetrySuppressingExecutor(2);
114169
MockMeterReaderConnector connector = spy(getMockedConnector(true, () -> {
115-
try {
116-
Thread.sleep(timeout);
117-
} catch (InterruptedException e) {
118-
}
119-
throw new RuntimeException(new IOException("fucked up"));
170+
executorService.markSourceTask();
171+
readStarted.countDown();
172+
awaitUninterruptibly(releaseRead);
173+
throw new UncheckedIOException(new IOException("simulated read failure"));
120174
}));
121175
MeterDevice<Object> meter = getMeterDevice(connector);
122176
@SuppressWarnings("unchecked")
123177
Consumer<Throwable> errorHandler = mock(Consumer.class);
124178
RxJavaPlugins.setErrorHandler(errorHandler);
125179
MeterValueListener changeListener = Mockito.mock(MeterValueListener.class);
180+
CountDownLatch timeoutOccurred = new CountDownLatch(1);
181+
doAnswer(invocation -> {
182+
executorService.suppressNextRetry();
183+
timeoutOccurred.countDown();
184+
return null;
185+
}).when(changeListener).errorOccurred(any());
126186
meter.addValueChangeListener(changeListener);
127-
Disposable disposable = meter.readValues(timeout / 2, Executors.newScheduledThreadPool(2), period);
187+
188+
Disposable disposable = meter.readValues(timeout, executorService, Duration.ZERO);
189+
128190
try {
129-
verify(changeListener, timeout(timeout)).errorOccurred(any(TimeoutException.class));
130-
verifyNoMoreInteractions(errorHandler);
191+
await(readStarted, "The meter read did not start");
192+
await(timeoutOccurred, "The meter read did not time out");
193+
executorService.awaitRetrySuppressed(EVENT_TIMEOUT);
194+
releaseRead.countDown();
195+
executorService.awaitSourceTaskFinished(EVENT_TIMEOUT);
131196
} finally {
132-
disposable.dispose();
197+
releaseRead.countDown();
198+
dispose(disposable, executorService);
133199
}
200+
201+
verify(changeListener, times(1)).errorOccurred(any(TimeoutException.class));
202+
verifyNoInteractions(errorHandler);
134203
}
135204

136205
MockMeterReaderConnector getMockedConnector(boolean applyRetry, Supplier<Object> readNextSupplier) {
@@ -154,4 +223,40 @@ protected <Q extends Quantity<Q>> void populateValueCache(Object smlFile) {
154223
}
155224
};
156225
}
226+
227+
private void await(CountDownLatch latch, String failureMessage) {
228+
try {
229+
assertTrue(latch.await(EVENT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS), failureMessage);
230+
} catch (InterruptedException e) {
231+
Thread.currentThread().interrupt();
232+
throw new AssertionError("Interrupted while waiting for an asynchronous test event", e);
233+
}
234+
}
235+
236+
private void awaitUninterruptibly(CountDownLatch latch) {
237+
boolean interrupted = false;
238+
while (true) {
239+
try {
240+
latch.await();
241+
break;
242+
} catch (InterruptedException e) {
243+
interrupted = true;
244+
}
245+
}
246+
if (interrupted) {
247+
Thread.currentThread().interrupt();
248+
}
249+
}
250+
251+
private void dispose(Disposable disposable, ScheduledExecutorService executorService) {
252+
disposable.dispose();
253+
executorService.shutdownNow();
254+
try {
255+
assertTrue(executorService.awaitTermination(EVENT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS),
256+
"Executor did not terminate");
257+
} catch (InterruptedException e) {
258+
Thread.currentThread().interrupt();
259+
throw new AssertionError("Interrupted while terminating the executor", e);
260+
}
261+
}
157262
}

0 commit comments

Comments
 (0)