1212 */
1313package org .openhab .binding .smartmeter ;
1414
15+ import static org .junit .jupiter .api .Assertions .assertTrue ;
1516import static org .mockito .ArgumentMatchers .any ;
17+ import static org .mockito .ArgumentMatchers .anyInt ;
1618import static org .mockito .Mockito .*;
1719
1820import java .io .IOException ;
21+ import java .io .UncheckedIOException ;
1922import 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 ;
2126import java .util .concurrent .TimeoutException ;
2227import java .util .function .Supplier ;
2328
2429import javax .measure .Quantity ;
2530
2631import org .eclipse .jdt .annotation .NonNullByDefault ;
32+ import org .junit .jupiter .api .AfterEach ;
2733import org .junit .jupiter .api .Test ;
28- import org .mockito .ArgumentMatchers ;
2934import org .mockito .Mockito ;
3035import org .openhab .binding .smartmeter .connectors .ConnectorBase ;
3136import org .openhab .binding .smartmeter .connectors .IMeterReaderConnector ;
4752@ NonNullByDefault
4853public 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