1 module modbus.ut; 2 3 version (unittest): package: 4 5 import core.thread; 6 7 import std.array; 8 import std.algorithm; 9 import std.concurrency; 10 import std.conv; 11 import std.datetime.stopwatch; 12 import std.exception; 13 import std.format; 14 import std.random; 15 import std.range; 16 import std.stdio : stderr; 17 import std..string; 18 import std.random; 19 import std.process; 20 21 import modbus.connection; 22 import modbus.backend; 23 import modbus.protocol.master; 24 import modbus.msleep; 25 26 enum test_print_offset = " "; 27 28 void testPrint(string s) { stderr.writeln(test_print_offset, s); } 29 void testPrintf(string fmt="%s", Args...)(Args args) 30 { stderr.writefln!(test_print_offset~fmt)(args); } 31 32 void ut(alias fnc, string uname="", Args...)(Args args) 33 { 34 static if (uname.length) 35 enum name = uname; 36 else 37 enum name = __traits(identifier, fnc); 38 stderr.writefln!" >> run %s"(name); 39 fnc(args); 40 scope (success) stderr.writefln!" << success %s\n"(name); 41 scope (failure) stderr.writefln!" !! failure %s\n"(name); 42 } 43 44 enum mainTestMix = ` 45 stderr.writefln!"=== start %s test {{{\n"(__MODULE__); 46 scope (success) stderr.writefln!"}}} finish %s test ===\n"(__MODULE__); 47 scope (failure) stderr.writefln!"}}} fail %s test !!!"(__MODULE__); 48 `; 49 50 enum BUFFER_SIZE = 1024; 51 52 interface ComPipe 53 { 54 void open(); 55 void close(); 56 string command() const @property; 57 string[2] ports() const @property; 58 } 59 60 class SocatPipe : ComPipe 61 { 62 int bufferSize; 63 ProcessPipes pipe; 64 string[2] _ports = ["./tmp1.port", "./tmp2.port"]; 65 string _command; 66 67 this(int bs) 68 { 69 bufferSize = bs; 70 _command = format!"socat -b%d pty,raw,echo=0,link=%s pty,raw,echo=0,link=%s" 71 (bufferSize, _ports[0], _ports[1]); 72 } 73 74 override void close() 75 { 76 if (pipe.pid is null) return; 77 kill(pipe.pid); 78 } 79 80 override void open() 81 { 82 pipe = pipeShell(_command); 83 Thread.sleep(1000.msecs); // wait for socat create ports 84 } 85 86 override const @property 87 { 88 string command() { return _command; } 89 string[2] ports() { return _ports; } 90 } 91 } 92 93 class DefinedPorts : ComPipe 94 { 95 string[2] env; 96 string[2] _ports; 97 98 this(string[2] envNames = ["MODBUS_TEST_COMPORT1", "MODBUS_TEST_COMPORT2"]) 99 { env = envNames; } 100 101 override: 102 103 void open() 104 { 105 import std.process : environment; 106 import std.range : lockstep; 107 import std.algorithm : canFind; 108 109 import serialport; 110 auto lst = SerialPort.listAvailable; 111 112 foreach (ref e, ref p; lockstep(env[], _ports[])) 113 { 114 p = environment[e]; 115 enforce(lst.canFind(p), new Exception("unknown port '%s' in env var '%s'".format(p, e))); 116 } 117 } 118 119 void close() { } 120 121 string command() const @property 122 { 123 return "env: %s=%s, %s=%s".format( 124 env[0], _ports[0], 125 env[1], _ports[1] 126 ); 127 } 128 129 string[2] ports() const @property { return _ports; } 130 } 131 132 ComPipe getPlatformComPipe(int bufsz) 133 { 134 try 135 { 136 auto ret = new DefinedPorts; 137 ret.open(); 138 return ret; 139 } 140 catch (Throwable e) 141 { 142 stderr.writeln(); 143 stderr.writeln(" error while open predefined ports: ", e.msg); 144 version (Posix) return new SocatPipe(bufsz); 145 else return null; 146 } 147 } 148 149 // slave tests 150 151 import modbus; 152 153 unittest 154 { 155 mixin(mainTestMix); 156 157 ut!fiberVirtualPipeBasedTest(); 158 159 auto cp = getPlatformComPipe(BUFFER_SIZE); 160 161 if (cp is null) 162 { 163 stderr.writeln(" platform doesn't support real test"); 164 return; 165 } 166 167 stderr.writefln(" port source `%s`\n", cp.command); 168 try cp.open(); 169 catch (Exception e) stderr.writeln(" can't open com pipe: ", e.msg); 170 scope (exit) cp.close(); 171 stderr.writefln(" pipe ports: %s <=> %s", cp.ports[0], cp.ports[1]); 172 173 ut!fiberSerialportBasedTest(cp.ports); 174 } 175 176 void fiberVirtualPipeBasedTest() 177 { 178 auto con = virtualPipeConnection(256, "test"); 179 baseModbusTest!RTU(con[0], con[1]); 180 baseModbusTest!TCP(con[0], con[1]); 181 } 182 183 void fiberSerialportBasedTest(string[2] ports) 184 { 185 enum spmode = "8N1"; 186 187 import std.typecons : scoped; 188 import serialport; 189 import modbus.connection.rtu; 190 191 auto p1 = scoped!SerialPortFR(ports[0], spmode); 192 auto p2 = scoped!SerialPortFR(ports[1], spmode); 193 p1.flush(); 194 p2.flush(); 195 196 alias SPC = SerialPortConnection; 197 198 baseModbusTest!RTU(new SPC(p1), new SPC(p2)); 199 baseModbusTest!TCP(new SPC(p1), new SPC(p2)); 200 } 201 202 void baseModbusTest(Be: Backend)(Connection masterCon, Connection slaveCon, Duration rtm=500.msecs) 203 { 204 enum DN = 13; 205 testPrintf!"BE: %s"(Be.classinfo.name); 206 207 enum dln = TestModbusSlaveDevice.Data.sizeof / 2; 208 ushort[] origin = void; 209 TestModbusSlaveDevice.Data* originData; 210 211 bool finish; 212 213 void mfnc() 214 { 215 auto master = new ModbusMaster(new Be, masterCon); 216 masterCon.readTimeout = rtm; 217 Fiber.getThis.yield(); 218 auto dt = master.readInputRegisters(DN, 0, dln); 219 assert( equal(origin, dt) ); 220 assert( equal(origin[2..4], master.readHoldingRegisters(DN, 2, 2)) ); 221 222 master.writeMultipleRegisters(DN, 2, [0xBEAF, 0xDEAD]); 223 assert((*originData).value2 == 0xDEADBEAF); 224 master.writeSingleRegister(DN, 15, 0xABCD); 225 assert((*originData).usv[1] == 0xABCD); 226 227 finish = true; 228 } 229 230 void sfnc() 231 { 232 auto device = new TestModbusSlaveDevice(DN); 233 originData = &device.data; 234 235 auto model = new MultiDevModbusSlaveModel; 236 model.devices ~= device; 237 238 auto slave = new ModbusSlave(model, new Be, slaveCon); 239 Fiber.getThis.yield(); 240 while (!finish) 241 { 242 origin = cast(ushort[])((cast(void*)&device.data)[0..dln*2]); 243 slave.iterate(); 244 Fiber.getThis.yield(); 245 } 246 } 247 248 auto mfiber = new Fiber(&mfnc); 249 auto sfiber = new Fiber(&sfnc); 250 251 bool work = true; 252 int step; 253 while (work) 254 { 255 alias TERM = Fiber.State.TERM; 256 if (mfiber.state != TERM) mfiber.call; 257 //stderr.writeln(getBuffer()); 258 if (sfiber.state != TERM) sfiber.call; 259 260 step++; 261 //stderr.writeln(getBuffer()); 262 Thread.sleep(10.msecs); 263 if (mfiber.state == TERM && sfiber.state == TERM) 264 work = false; 265 } 266 }