]> git.basschouten.com Git - openhab-addons.git/blob
ae573be3041efd8152be4ca86837cc928cbd14ce
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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 package org.openhab.io.transport.modbus.test;
14
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.junit.jupiter.api.Assertions.assertNotSame;
18 import static org.mockito.ArgumentMatchers.any;
19 import static org.mockito.Mockito.*;
20
21 import java.io.File;
22 import java.net.DatagramSocket;
23 import java.net.InetAddress;
24 import java.net.Socket;
25 import java.net.SocketException;
26 import java.net.UnknownHostException;
27 import java.util.HashMap;
28 import java.util.function.LongSupplier;
29
30 import org.apache.commons.lang.NotImplementedException;
31 import org.junit.jupiter.api.AfterEach;
32 import org.junit.jupiter.api.BeforeEach;
33 import org.junit.jupiter.api.extension.ExtendWith;
34 import org.mockito.Spy;
35 import org.mockito.junit.jupiter.MockitoExtension;
36 import org.mockito.junit.jupiter.MockitoSettings;
37 import org.mockito.quality.Strictness;
38 import org.openhab.core.test.java.JavaTest;
39 import org.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
40 import org.openhab.io.transport.modbus.endpoint.ModbusTCPSlaveEndpoint;
41 import org.openhab.io.transport.modbus.internal.ModbusManagerImpl;
42
43 import gnu.io.SerialPort;
44 import net.wimpi.modbus.Modbus;
45 import net.wimpi.modbus.ModbusCoupler;
46 import net.wimpi.modbus.ModbusIOException;
47 import net.wimpi.modbus.io.ModbusTransport;
48 import net.wimpi.modbus.msg.ModbusRequest;
49 import net.wimpi.modbus.net.ModbusSerialListener;
50 import net.wimpi.modbus.net.ModbusTCPListener;
51 import net.wimpi.modbus.net.ModbusUDPListener;
52 import net.wimpi.modbus.net.SerialConnection;
53 import net.wimpi.modbus.net.SerialConnectionFactory;
54 import net.wimpi.modbus.net.TCPSlaveConnection;
55 import net.wimpi.modbus.net.TCPSlaveConnection.ModbusTCPTransportFactory;
56 import net.wimpi.modbus.net.TCPSlaveConnectionFactory;
57 import net.wimpi.modbus.net.UDPSlaveTerminal;
58 import net.wimpi.modbus.net.UDPSlaveTerminal.ModbusUDPTransportFactoryImpl;
59 import net.wimpi.modbus.net.UDPSlaveTerminalFactory;
60 import net.wimpi.modbus.net.UDPTerminal;
61 import net.wimpi.modbus.procimg.SimpleProcessImage;
62 import net.wimpi.modbus.util.AtomicCounter;
63 import net.wimpi.modbus.util.SerialParameters;
64
65 /**
66  * @author Sami Salonen - Initial contribution
67  */
68 @ExtendWith(MockitoExtension.class)
69 @MockitoSettings(strictness = Strictness.WARN)
70 public class IntegrationTestSupport extends JavaTest {
71
72     public enum ServerType {
73         TCP,
74         UDP,
75         SERIAL
76     }
77
78     /**
79      * Servers to test
80      * Serial is system dependent
81      */
82     public static final ServerType[] TEST_SERVERS = new ServerType[] { ServerType.TCP
83             // ServerType.UDP,
84             // ServerType.SERIAL
85     };
86
87     // One can perhaps test SERIAL with https://github.com/freemed/tty0tty
88     // and using those virtual ports? Not the same thing as real serial device of course
89     private static String SERIAL_SERVER_PORT = "/dev/pts/7";
90     private static String SERIAL_CLIENT_PORT = "/dev/pts/8";
91
92     private static SerialParameters SERIAL_PARAMETERS_CLIENT = new SerialParameters(SERIAL_CLIENT_PORT, 115200,
93             SerialPort.FLOWCONTROL_NONE, SerialPort.FLOWCONTROL_NONE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
94             SerialPort.PARITY_NONE, Modbus.SERIAL_ENCODING_ASCII, false, 1000);
95
96     private static SerialParameters SERIAL_PARAMETERS_SERVER = new SerialParameters(SERIAL_SERVER_PORT,
97             SERIAL_PARAMETERS_CLIENT.getBaudRate(), SERIAL_PARAMETERS_CLIENT.getFlowControlIn(),
98             SERIAL_PARAMETERS_CLIENT.getFlowControlOut(), SERIAL_PARAMETERS_CLIENT.getDatabits(),
99             SERIAL_PARAMETERS_CLIENT.getStopbits(), SERIAL_PARAMETERS_CLIENT.getParity(),
100             SERIAL_PARAMETERS_CLIENT.getEncoding(), SERIAL_PARAMETERS_CLIENT.isEcho(), 1000);
101
102     static {
103         System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
104         System.setProperty("gnu.io.rxtx.SerialPorts", SERIAL_SERVER_PORT + File.pathSeparator + SERIAL_CLIENT_PORT);
105     }
106
107     /**
108      * Max time to wait for connections/requests from client
109      */
110     protected int MAX_WAIT_REQUESTS_MILLIS = 1000;
111
112     /**
113      * The server runs in single thread, only one connection is accepted at a time.
114      * This makes the tests as strict as possible -- connection must be closed.
115      */
116     private static final int SERVER_THREADS = 1;
117     protected static int SLAVE_UNIT_ID = 1;
118
119     private static AtomicCounter udpServerIndex = new AtomicCounter(0);
120
121     protected @Spy TCPSlaveConnectionFactory tcpConnectionFactory = new TCPSlaveConnectionFactoryImpl();
122     protected @Spy UDPSlaveTerminalFactory udpTerminalFactory = new UDPSlaveTerminalFactoryImpl();
123     protected @Spy SerialConnectionFactory serialConnectionFactory = new SerialConnectionFactoryImpl();
124
125     protected ResultCaptor<ModbusRequest> modbustRequestCaptor;
126
127     protected ModbusTCPListener tcpListener;
128     protected ModbusUDPListener udpListener;
129     protected ModbusSerialListener serialListener;
130     protected SimpleProcessImage spi;
131     protected int tcpModbusPort = -1;
132     protected int udpModbusPort = -1;
133     protected ServerType serverType = ServerType.TCP;
134     protected long artificialServerWait = 0;
135
136     protected NonOSGIModbusManager modbusManager;
137
138     private Thread serialServerThread = new Thread("ModbusTransportTestsSerialServer") {
139         @Override
140         public void run() {
141             serialListener = new ModbusSerialListener(SERIAL_PARAMETERS_SERVER);
142         }
143     };
144
145     protected static InetAddress localAddress() throws UnknownHostException {
146         return InetAddress.getByName("127.0.0.1");
147     }
148
149     @BeforeEach
150     public void setUp() throws Exception {
151         modbustRequestCaptor = new ResultCaptor<>(new LongSupplier() {
152
153             @Override
154             public long getAsLong() {
155                 return artificialServerWait;
156             }
157         });
158         modbusManager = new NonOSGIModbusManager();
159         startServer();
160     }
161
162     @AfterEach
163     public void tearDown() {
164         stopServer();
165         modbusManager.close();
166     }
167
168     protected void waitForRequests(int expectedRequestCount) {
169         waitForAssert(
170                 () -> assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(expectedRequestCount))),
171                 MAX_WAIT_REQUESTS_MILLIS, 10);
172     }
173
174     protected void waitForConnectionsReceived(int expectedConnections) {
175         waitForAssert(() -> {
176             if (ServerType.TCP.equals(serverType)) {
177                 verify(tcpConnectionFactory, times(expectedConnections)).create(any(Socket.class));
178             } else if (ServerType.UDP.equals(serverType)) {
179                 // No-op
180                 // verify(udpTerminalFactory, times(expectedConnections)).create(any(InetAddress.class),
181                 // any(Integer.class));
182             } else if (ServerType.SERIAL.equals(serverType)) {
183                 // No-op
184             } else {
185                 throw new NotImplementedException();
186             }
187         }, MAX_WAIT_REQUESTS_MILLIS, 10);
188     }
189
190     private void startServer() throws UnknownHostException, InterruptedException {
191         spi = new SimpleProcessImage();
192         ModbusCoupler.getReference().setProcessImage(spi);
193         ModbusCoupler.getReference().setMaster(false);
194         ModbusCoupler.getReference().setUnitID(SLAVE_UNIT_ID);
195
196         if (ServerType.TCP.equals(serverType)) {
197             startTCPServer();
198         } else if (ServerType.UDP.equals(serverType)) {
199             startUDPServer();
200         } else if (ServerType.SERIAL.equals(serverType)) {
201             startSerialServer();
202         } else {
203             throw new NotImplementedException();
204         }
205     }
206
207     private void stopServer() {
208         if (ServerType.TCP.equals(serverType)) {
209             tcpListener.stop();
210         } else if (ServerType.UDP.equals(serverType)) {
211             udpListener.stop();
212             System.err.println(udpModbusPort);
213         } else if (ServerType.SERIAL.equals(serverType)) {
214             try {
215                 serialServerThread.join(100);
216             } catch (InterruptedException e) {
217                 System.err.println("Serial server thread .join() interrupted! Will interrupt it now.");
218             }
219             serialServerThread.interrupt();
220         } else {
221             throw new NotImplementedException();
222         }
223     }
224
225     private void startUDPServer() throws UnknownHostException, InterruptedException {
226         udpListener = new ModbusUDPListener(localAddress(), udpTerminalFactory);
227         for (int portCandidate = 10000 + udpServerIndex.increment(); portCandidate < 20000; portCandidate++) {
228             try {
229                 DatagramSocket socket = new DatagramSocket(portCandidate);
230                 socket.close();
231                 udpListener.setPort(portCandidate);
232                 break;
233             } catch (SocketException e) {
234                 continue;
235             }
236         }
237
238         udpListener.start();
239         waitForUDPServerStartup();
240         assertNotSame(-1, udpModbusPort);
241         assertNotSame(0, udpModbusPort);
242     }
243
244     private void waitForUDPServerStartup() throws InterruptedException {
245         // Query server port. It seems to take time (probably due to thread starting)
246         waitFor(() -> udpListener.getLocalPort() > 0, 5, 10_000);
247         udpModbusPort = udpListener.getLocalPort();
248     }
249
250     private void startTCPServer() throws UnknownHostException, InterruptedException {
251         // Serve single user at a time
252         tcpListener = new ModbusTCPListener(SERVER_THREADS, localAddress(), tcpConnectionFactory);
253         // Use any open port
254         tcpListener.setPort(0);
255         tcpListener.start();
256         // Query server port. It seems to take time (probably due to thread starting)
257         waitForTCPServerStartup();
258         assertNotSame(-1, tcpModbusPort);
259         assertNotSame(0, tcpModbusPort);
260     }
261
262     private void waitForTCPServerStartup() throws InterruptedException {
263         waitFor(() -> tcpListener.getLocalPort() > 0, 10_000, 5);
264         tcpModbusPort = tcpListener.getLocalPort();
265     }
266
267     private void startSerialServer() throws UnknownHostException, InterruptedException {
268         serialServerThread.start();
269         Thread.sleep(1000);
270     }
271
272     public ModbusSlaveEndpoint getEndpoint() {
273         assert tcpModbusPort > 0;
274         return new ModbusTCPSlaveEndpoint("127.0.0.1", tcpModbusPort);
275     }
276
277     /**
278      * Transport factory that spies the created transport items
279      */
280     public class SpyingModbusTCPTransportFactory extends ModbusTCPTransportFactory {
281
282         @Override
283         public ModbusTransport create(Socket socket) {
284             ModbusTransport transport = spy(super.create(socket));
285             // Capture requests produced by our server transport
286             try {
287                 doAnswer(modbustRequestCaptor).when(transport).readRequest();
288             } catch (ModbusIOException e) {
289                 throw new RuntimeException(e);
290             }
291             return transport;
292         }
293     }
294
295     public class SpyingModbusUDPTransportFactory extends ModbusUDPTransportFactoryImpl {
296
297         @Override
298         public ModbusTransport create(UDPTerminal terminal) {
299             ModbusTransport transport = spy(super.create(terminal));
300             // Capture requests produced by our server transport
301             try {
302                 doAnswer(modbustRequestCaptor).when(transport).readRequest();
303             } catch (ModbusIOException e) {
304                 throw new RuntimeException(e);
305             }
306             return transport;
307         }
308     }
309
310     public class TCPSlaveConnectionFactoryImpl implements TCPSlaveConnectionFactory {
311
312         @Override
313         public TCPSlaveConnection create(Socket socket) {
314             return new TCPSlaveConnection(socket, new SpyingModbusTCPTransportFactory());
315         }
316     }
317
318     public class UDPSlaveTerminalFactoryImpl implements UDPSlaveTerminalFactory {
319
320         @Override
321         public UDPSlaveTerminal create(InetAddress interfac, int port) {
322             UDPSlaveTerminal terminal = new UDPSlaveTerminal(interfac, new SpyingModbusUDPTransportFactory(), 1);
323             terminal.setLocalPort(port);
324             return terminal;
325         }
326     }
327
328     public class SerialConnectionFactoryImpl implements SerialConnectionFactory {
329         @Override
330         public SerialConnection create(SerialParameters parameters) {
331             SerialConnection serialConnection = new SerialConnection(parameters) {
332                 @Override
333                 public ModbusTransport getModbusTransport() {
334                     ModbusTransport transport = spy(super.getModbusTransport());
335                     try {
336                         doAnswer(modbustRequestCaptor).when(transport).readRequest();
337                     } catch (ModbusIOException e) {
338                         throw new RuntimeException(e);
339                     }
340                     return transport;
341                 }
342             };
343             return serialConnection;
344         }
345     }
346
347     public static class NonOSGIModbusManager extends ModbusManagerImpl implements AutoCloseable {
348         public NonOSGIModbusManager() {
349             activate(new HashMap<>());
350         }
351
352         @Override
353         public void close() {
354             deactivate();
355         }
356     }
357 }