2 * Copyright (c) 2010-2020 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.io.transport.modbus.test;
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.*;
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;
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;
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;
66 * @author Sami Salonen - Initial contribution
68 @ExtendWith(MockitoExtension.class)
69 @MockitoSettings(strictness = Strictness.WARN)
70 public class IntegrationTestSupport extends JavaTest {
72 public enum ServerType {
80 * Serial is system dependent
82 public static final ServerType[] TEST_SERVERS = new ServerType[] { ServerType.TCP
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";
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);
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);
103 System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace");
104 System.setProperty("gnu.io.rxtx.SerialPorts", SERIAL_SERVER_PORT + File.pathSeparator + SERIAL_CLIENT_PORT);
108 * Max time to wait for connections/requests from client
110 protected int MAX_WAIT_REQUESTS_MILLIS = 1000;
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.
116 private static final int SERVER_THREADS = 1;
117 protected static int SLAVE_UNIT_ID = 1;
119 private static AtomicCounter udpServerIndex = new AtomicCounter(0);
121 protected @Spy TCPSlaveConnectionFactory tcpConnectionFactory = new TCPSlaveConnectionFactoryImpl();
122 protected @Spy UDPSlaveTerminalFactory udpTerminalFactory = new UDPSlaveTerminalFactoryImpl();
123 protected @Spy SerialConnectionFactory serialConnectionFactory = new SerialConnectionFactoryImpl();
125 protected ResultCaptor<ModbusRequest> modbustRequestCaptor;
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;
136 protected NonOSGIModbusManager modbusManager;
138 private Thread serialServerThread = new Thread("ModbusTransportTestsSerialServer") {
141 serialListener = new ModbusSerialListener(SERIAL_PARAMETERS_SERVER);
145 protected static InetAddress localAddress() throws UnknownHostException {
146 return InetAddress.getByName("127.0.0.1");
150 public void setUp() throws Exception {
151 modbustRequestCaptor = new ResultCaptor<>(new LongSupplier() {
154 public long getAsLong() {
155 return artificialServerWait;
158 modbusManager = new NonOSGIModbusManager();
163 public void tearDown() {
165 modbusManager.close();
168 protected void waitForRequests(int expectedRequestCount) {
170 () -> assertThat(modbustRequestCaptor.getAllReturnValues().size(), is(equalTo(expectedRequestCount))),
171 MAX_WAIT_REQUESTS_MILLIS, 10);
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)) {
180 // verify(udpTerminalFactory, times(expectedConnections)).create(any(InetAddress.class),
181 // any(Integer.class));
182 } else if (ServerType.SERIAL.equals(serverType)) {
185 throw new NotImplementedException();
187 }, MAX_WAIT_REQUESTS_MILLIS, 10);
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);
196 if (ServerType.TCP.equals(serverType)) {
198 } else if (ServerType.UDP.equals(serverType)) {
200 } else if (ServerType.SERIAL.equals(serverType)) {
203 throw new NotImplementedException();
207 private void stopServer() {
208 if (ServerType.TCP.equals(serverType)) {
210 } else if (ServerType.UDP.equals(serverType)) {
212 System.err.println(udpModbusPort);
213 } else if (ServerType.SERIAL.equals(serverType)) {
215 serialServerThread.join(100);
216 } catch (InterruptedException e) {
217 System.err.println("Serial server thread .join() interrupted! Will interrupt it now.");
219 serialServerThread.interrupt();
221 throw new NotImplementedException();
225 private void startUDPServer() throws UnknownHostException, InterruptedException {
226 udpListener = new ModbusUDPListener(localAddress(), udpTerminalFactory);
227 for (int portCandidate = 10000 + udpServerIndex.increment(); portCandidate < 20000; portCandidate++) {
229 DatagramSocket socket = new DatagramSocket(portCandidate);
231 udpListener.setPort(portCandidate);
233 } catch (SocketException e) {
239 waitForUDPServerStartup();
240 assertNotSame(-1, udpModbusPort);
241 assertNotSame(0, udpModbusPort);
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();
250 private void startTCPServer() throws UnknownHostException, InterruptedException {
251 // Serve single user at a time
252 tcpListener = new ModbusTCPListener(SERVER_THREADS, localAddress(), tcpConnectionFactory);
254 tcpListener.setPort(0);
256 // Query server port. It seems to take time (probably due to thread starting)
257 waitForTCPServerStartup();
258 assertNotSame(-1, tcpModbusPort);
259 assertNotSame(0, tcpModbusPort);
262 private void waitForTCPServerStartup() throws InterruptedException {
263 waitFor(() -> tcpListener.getLocalPort() > 0, 10_000, 5);
264 tcpModbusPort = tcpListener.getLocalPort();
267 private void startSerialServer() throws UnknownHostException, InterruptedException {
268 serialServerThread.start();
272 public ModbusSlaveEndpoint getEndpoint() {
273 assert tcpModbusPort > 0;
274 return new ModbusTCPSlaveEndpoint("127.0.0.1", tcpModbusPort);
278 * Transport factory that spies the created transport items
280 public class SpyingModbusTCPTransportFactory extends ModbusTCPTransportFactory {
283 public ModbusTransport create(Socket socket) {
284 ModbusTransport transport = spy(super.create(socket));
285 // Capture requests produced by our server transport
287 doAnswer(modbustRequestCaptor).when(transport).readRequest();
288 } catch (ModbusIOException e) {
289 throw new RuntimeException(e);
295 public class SpyingModbusUDPTransportFactory extends ModbusUDPTransportFactoryImpl {
298 public ModbusTransport create(UDPTerminal terminal) {
299 ModbusTransport transport = spy(super.create(terminal));
300 // Capture requests produced by our server transport
302 doAnswer(modbustRequestCaptor).when(transport).readRequest();
303 } catch (ModbusIOException e) {
304 throw new RuntimeException(e);
310 public class TCPSlaveConnectionFactoryImpl implements TCPSlaveConnectionFactory {
313 public TCPSlaveConnection create(Socket socket) {
314 return new TCPSlaveConnection(socket, new SpyingModbusTCPTransportFactory());
318 public class UDPSlaveTerminalFactoryImpl implements UDPSlaveTerminalFactory {
321 public UDPSlaveTerminal create(InetAddress interfac, int port) {
322 UDPSlaveTerminal terminal = new UDPSlaveTerminal(interfac, new SpyingModbusUDPTransportFactory(), 1);
323 terminal.setLocalPort(port);
328 public class SerialConnectionFactoryImpl implements SerialConnectionFactory {
330 public SerialConnection create(SerialParameters parameters) {
331 SerialConnection serialConnection = new SerialConnection(parameters) {
333 public ModbusTransport getModbusTransport() {
334 ModbusTransport transport = spy(super.getModbusTransport());
336 doAnswer(modbustRequestCaptor).when(transport).readRequest();
337 } catch (ModbusIOException e) {
338 throw new RuntimeException(e);
343 return serialConnection;
347 public static class NonOSGIModbusManager extends ModbusManagerImpl implements AutoCloseable {
348 public NonOSGIModbusManager() {
349 activate(new HashMap<>());
353 public void close() {