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