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 }