1 module serialportx.ut;
2 
3 version (unittest): private:
4 
5 import serialport;
6 
7 import std.range;
8 import std.concurrency;
9 import std.exception;
10 import std.datetime;
11 import std.conv;
12 import std.string;
13 import std.stdio;
14 import std.random;
15 import std.process;
16 import core.thread;
17 
18 enum BUFFER_SIZE = 1024;
19 
20 interface ComPipe
21 {
22     void open();
23     void close();
24     string command() const @property;
25     string[2] ports() const @property;
26 }
27 
28 class SocatPipe : ComPipe
29 {
30     int bufferSize;
31     ProcessPipes pipe;
32     string[2] _ports;
33     string _command;
34 
35     this(int bs)
36     {
37         bufferSize = bs;
38         _command = ("socat -d -d -b%d pty,raw,"~
39                     "echo=0 pty,raw,echo=0").format(bufferSize);
40     }
41 
42     static string parsePort(string ln)
43     {
44         auto ret = ln.split[$-1];
45         enforce(ret.startsWith("/dev/"),
46         "unexpected last word in output line '%s'".format(ln));
47         return ret;
48     }
49 
50     override void close()
51     {
52         if (pipe.pid is null) return;
53         kill(pipe.pid);
54     }
55 
56     override void open()
57     {
58         pipe = pipeShell(_command);
59         _ports[0] = parsePort(pipe.stderr.readln.strip);
60         _ports[1] = parsePort(pipe.stderr.readln.strip);
61     }
62 
63     override const @property
64     {
65         string command() { return _command; }
66         string[2] ports() { return _ports; }
67     }
68 }
69 
70 unittest
71 {
72     enum socat_out_ln = "2018/03/08 02:56:58 socat[30331] N PTY is /dev/pts/1";
73     assert(SocatPipe.parsePort(socat_out_ln) == "/dev/pts/1");
74     assertThrown(SocatPipe.parsePort("some string"));
75 }
76 
77 class DefinedPorts : ComPipe
78 {
79     string[2] env;
80     string[2] _ports;
81 
82     this(string[2] envNames = ["SERIALPORT_TEST_PORT1", "SERIALPORT_TEST_PORT2"])
83     { env = envNames; }
84 
85 override:
86 
87     void open()
88     {
89         import std.process : environment;
90         import std.range : lockstep;
91         import std.algorithm : canFind;
92 
93         auto lst = SerialPort.listAvailable;
94 
95         foreach (ref e, ref p; lockstep(env[], _ports[]))
96         {
97             p = environment[e];
98             enforce(lst.canFind(p), new Exception("unknown port '%s' in env var '%s'".format(p, e)));
99         }
100     }
101 
102     void close() { }
103 
104     string command() const @property
105     {
106         return "env: %s=%s, %s=%s".format(
107             env[0], _ports[0],
108             env[1], _ports[1]
109         );
110     }
111 
112     string[2] ports() const @property { return _ports; }
113 }
114 
115 ComPipe getPlatformComPipe(int bufsz)
116 {
117     stderr.writeln("available ports count: ", SerialPort.listAvailable.length);
118 
119     try
120     {
121         auto ret = new DefinedPorts;
122         ret.open();
123         return ret;
124     }
125     catch (Exception e)
126     {
127         stderr.writeln();
128         stderr.writeln("error while open predefined ports: ", e.msg);
129 
130         version (Posix) return new SocatPipe(bufsz);
131         else return null;
132     }
133 }
134 
135 // real test main
136 //version (realtest)
137 unittest
138 {
139     stderr.writeln("=== start real test ===\n");
140     scope (success) stderr.writeln("=== finish real test ===");
141     scope (failure) stderr.writeln("!!!  fail real test  !!!");
142     auto cp = getPlatformComPipe(BUFFER_SIZE);
143     if (cp is null)
144     {
145         stderr.writeln("platform doesn't support real test");
146         return;
147     }
148 
149     stderr.writefln("port source `%s`\n", cp.command);
150 
151     void reopen()
152     {
153         cp.close();
154         Thread.sleep(30.msecs);
155         cp.open();
156         stderr.writefln("pipe ports: %s <=> %s", cp.ports[0], cp.ports[1]);
157     }
158 
159     reopen();
160 
161     utCall!(threadTest!SerialPortFR)("thread test for fiber ready", cp.ports);
162     utCall!(threadTest!SerialPortBlk)("thread test for block", cp.ports);
163     utCall!testNonBlock("test non block", cp.ports);
164     utCall!fiberTest("fiber test", cp.ports);
165     utCall!fiberTest2("fiber test 2", cp.ports);
166     utCall!readTimeoutTest("read timeout test", cp.ports);
167     alias rttc = readTimeoutTestConfig;
168     alias rttc2 = readTimeoutTestConfig2;
169     utCall!(rttc!SerialPortFR)( "read timeout test for FR  cr=zero", cp.ports, SerialPort.CanRead.zero);
170     utCall!(rttc!SerialPortBlk)("read timeout test for Blk cr=zero", cp.ports, SerialPort.CanRead.zero);
171     utCall!(rttc!SerialPortFR)( "read timeout test for FR  cr=anyNonZero", cp.ports, SerialPort.CanRead.anyNonZero);
172     utCall!(rttc!SerialPortBlk)("read timeout test for Blk cr=anyNonZero", cp.ports, SerialPort.CanRead.anyNonZero);
173     utCall!(rttc!SerialPortFR)( "read timeout test for FR  cr=allOrNothing", cp.ports, SerialPort.CanRead.allOrNothing);
174     utCall!(rttc!SerialPortBlk)("read timeout test for Blk cr=allOrNothing", cp.ports, SerialPort.CanRead.allOrNothing);
175     utCall!(rttc2!SerialPortFR)( "read timeout test 2 for FR  cr=zero", cp.ports, SerialPort.CanRead.zero);
176     utCall!(rttc2!SerialPortBlk)("read timeout test 2 for Blk cr=zero", cp.ports, SerialPort.CanRead.zero);
177     utCall!(rttc2!SerialPortFR)( "read timeout test 2 for FR  cr=anyNonZero", cp.ports, SerialPort.CanRead.anyNonZero);
178     utCall!(rttc2!SerialPortBlk)("read timeout test 2 for Blk cr=anyNonZero", cp.ports, SerialPort.CanRead.anyNonZero);
179     utCall!(rttc2!SerialPortFR)( "read timeout test 2 for FR  cr=allOrNothing", cp.ports, SerialPort.CanRead.allOrNothing);
180     utCall!(rttc2!SerialPortBlk)("read timeout test 2 for Blk cr=allOrNothing", cp.ports, SerialPort.CanRead.allOrNothing);
181     utCall!(fiberSleepFuncTest)("fiber sleep func test", cp.ports);
182 }
183 
184 unittest
185 {
186     enum name = "/some/path/to/notexisting/device";
187     auto e = enforce(collectException(new SerialPortBlk(name, 19200)), "exception not thrown");
188     auto sce = cast(SysCallException)e;
189     assert (sce !is null);
190     assert (sce.port == name, "wrong name");
191     version (Posix)
192     {
193         assert(sce.fnc == "open", "'" ~ sce.fnc ~ "' is not 'open'");
194         assert(sce.err == 2, "unexpectable errno %d".format(sce.err));
195     }
196     auto exp = format!"call '%s' (%s) failed: error %d"(sce.fnc, name, sce.err);
197     if (!e.msg.startsWith(exp))
198     {
199         import std.stdio : stderr;
200         stderr.writeln("exp: ", exp);
201         stderr.writeln("msg: ", e.msg);
202         assert(0, "wrong msg");
203     }
204 }
205 
206 void testPrint(Args...)(Args args) { stderr.write("    "); stderr.writeln(args); }
207 void testPrintf(Args...)(Args args) { stderr.write("    "); stderr.writefln(args); }
208 
209 auto utCall(alias fnc, Args...)(string name, Args args)
210 {
211     stderr.writefln(">>> run %s", name);
212     scope (success) stderr.writefln("<<< success %s\n", name);
213     scope (failure) stderr.writefln("!!! failure %s\n", name);
214     return fnc(args);
215 }
216 
217 void threadTest(SPT)(string[2] ports)
218 {
219     assert(SerialPort.listAvailable.length != 0);
220 
221     static struct ExcStruct { string msg, type; }
222 
223     static void echoThread(string port)
224     {
225         void[BUFFER_SIZE] buffer = void;
226         auto com = new SPT(port, "2400:8N1");
227         scope (exit) com.close();
228         com.flush();
229 
230         com.set(1200);
231         assert(com.config.baudRate == 1200);
232 
233         com.baudRate = 38_400;
234         assert(com.config.baudRate == 38_400);
235 
236         bool work = true;
237         com.readTimeout = 1000.msecs;
238 
239         bool needRead;
240 
241         while (work)
242         {
243             try
244             {
245                 if (needRead)
246                 {
247                     Thread.sleep(500.msecs);
248                     auto data = com.read(buffer, com.CanRead.zero);
249 
250                     if (data.length)
251                     {
252                         testPrint("child readed: ", cast(string)(data.idup));
253                         send(ownerTid, cast(string)(data.idup));
254                     }
255                 }
256 
257                 receiveTimeout(500.msecs,
258                     (SPConfig cfg)
259                     {
260                         com.config = cfg;
261                         testPrint("child get cfg: ", cfg.mode);
262                     },
263                     (bool nr)
264                     {
265                         if (nr) needRead = true;
266                         else
267                         {
268                             work = false;
269                             needRead = false;
270                         }
271                         testPrint("get needRead ", nr);
272                     },
273                     (OwnerTerminated e) { work = false; }
274                 );
275             }
276             catch (Throwable e)
277             {
278                 work = false;
279                 testPrint("exception in child: ", e);
280                 send(ownerTid, ExcStruct(e.msg, e.classinfo.stringof));
281             }
282         }
283     }
284 
285     auto t = spawnLinked(&echoThread, ports[1]);
286 
287     auto com = new SPT(ports[0], 19_200);
288     com.flush();
289 
290     assert(com.baudRate == 19_200);
291     assert(com.dataBits == DataBits.data8);
292     assert(com.parity == Parity.none);
293     assert(com.stopBits == StopBits.one);
294 
295     assert(com.config.baudRate == 19_200);
296     assert(com.config.dataBits == DataBits.data8);
297     assert(com.config.parity == Parity.none);
298     assert(com.config.stopBits == StopBits.one);
299 
300     scope (exit) com.close();
301 
302     string[] list;
303 
304     const sets = [
305         SPConfig(38_400),
306         SPConfig(2400),
307         SPConfig.parse("19200:8N2"),
308     ];
309 
310     auto cfg = SPConfig(38_400);
311     com.config = cfg;
312     send(t, cfg);
313 
314     Thread.sleep(1000.msecs);
315 
316     string msg = sets.front.mode;
317     com.write(msg);
318 
319     bool work = true;
320     send(t, true);
321     while (work)
322     {
323         receive(
324             (string rec)
325             {
326                 enforce(rec == msg, "break message: '%s' != '%s'".format(msg, rec));
327 
328                 if (list.empty)
329                 {
330                     testPrint("owner send data finish");
331                     send(t, false);
332                 }
333                 else
334                 {
335                     msg = list.front;
336                     list.popFront();
337                 }
338 
339                 com.write(msg);
340                 testPrint("owner write msg to com: ", msg);
341             },
342             (ExcStruct e) { throw new Exception("%s:%s".format(e.type, e.msg)); },
343             (LinkTerminated e)
344             {
345                 work = false;
346                 testPrintf("link terminated for %s, child tid %s", e.tid, t);
347                 //assert(e.tid == t);
348             }
349         );
350     }
351 }
352 
353 void testNonBlock(string[2] ports)
354 {
355     import std.datetime.stopwatch : StopWatch, AutoStart;
356     enum mode = "38400:8N1";
357 
358     const data = "1234567890987654321qazxswedcvfrtgbnhyujm,ki";
359 
360     static void thfunc(string port)
361     {
362         auto com = new SerialPortNonBlk(port, mode);
363         scope (exit) com.close();
364 
365         void[1024] buffer = void;
366         size_t readed;
367 
368         const sw = StopWatch(AutoStart.yes);
369 
370         // flush
371         while (sw.peek < 10.msecs)
372         {
373             com.read(buffer);
374             Thread.sleep(1.msecs);
375         }
376 
377         while (sw.peek < 1.seconds)
378             readed += com.read(buffer[readed..$]).length;
379 
380         send(ownerTid, buffer[0..readed].idup);
381 
382         Thread.sleep(200.msecs);
383     }
384 
385     auto com = new SerialPortNonBlk(ports[0], 38_400, "8N1");
386     scope (exit) com.close();
387 
388     spawnLinked(&thfunc, ports[1]);
389 
390     Thread.sleep(100.msecs);
391 
392     size_t written;
393     while (written < data.length)
394         written += com.write(data[written..$]);
395 
396     receive((immutable(void)[] readed)
397     {
398         testPrint("readed: ", cast(string)readed);
399         testPrint("  data: ", data);
400         assert(cast(string)readed == data);
401     });
402 
403     receive((LinkTerminated e) { });
404 }
405 
406 class CF : Fiber
407 {
408     void[] data;
409 
410     SerialPortFR com;
411 
412     this(SerialPortFR com, size_t bufsize)
413     {
414         this.com = com;
415         this.com.flush();
416         this.data = new void[bufsize];
417         foreach (ref v; cast(ubyte[])data)
418             v = cast(ubyte)uniform(0, 128);
419         super(&run);
420     }
421 
422     abstract void run();
423 }
424 
425 class CFSlave : CF
426 {
427     void[] result;
428 
429     Duration readTimeout = 40.msecs;
430     Duration readGapTimeout = 100.msecs;
431 
432     this(SerialPortFR com, size_t bufsize)
433     { super(com, bufsize); }
434 
435     override void run()
436     {
437         testPrint("start read loop");
438         result = com.readContinues(data, readTimeout, readGapTimeout);
439         testPrint("finish read loop ("~result.length.to!string~")");
440     }
441 }
442 
443 class CFMaster : CF
444 {
445     CFSlave slave;
446 
447     Duration writeTimeout = 20.msecs;
448 
449     this(SerialPortFR com, size_t bufsize)
450     { super(com, bufsize); }
451 
452     override void run()
453     {
454         testPrint("start write loop ("~data.length.to!string~")");
455         com.writeTimeout = writeTimeout;
456         com.write(data);
457         testPrint("finish write loop");
458     }
459 }
460 
461 void fiberTest(string[2] ports)
462 {
463     auto slave = new CFSlave(new SerialPortFR(ports[0]), BUFFER_SIZE);
464     scope (exit) slave.com.close();
465     auto master = new CFMaster(new SerialPortFR(ports[1]), BUFFER_SIZE);
466     scope (exit) master.com.close();
467 
468     bool work = true;
469     int step;
470     while (work)
471     {
472         alias TERM = Fiber.State.TERM;
473         if (master.state != TERM) master.call;
474         if (slave.state != TERM) slave.call;
475 
476         step++;
477         Thread.sleep(30.msecs);
478         if (master.state == TERM && slave.state == TERM)
479         {
480             if (slave.result.length == master.data.length)
481             {
482                 import std.algorithm : equal;
483                 enforce(equal(cast(ubyte[])slave.result, cast(ubyte[])master.data));
484                 work = false;
485                 testPrint("basic loop steps: ", step);
486             }
487             else throw new Exception(text(slave.result, " != ", master.data));
488         }
489     }
490 }
491 
492 void fiberTest2(string[2] ports)
493 {
494     string mode = "9600:8N1";
495 
496     auto scom = new SerialPortFR(ports[0], 9600, "8N1");
497     auto mcom = new SerialPortFR(ports[1], "19200:8N1");
498     scope (exit) scom.close();
499     scope (exit) mcom.close();
500 
501     version (Posix)
502         assertThrown!UnsupportedException(scom.baudRate = 9200);
503 
504     scom.reopen(ports[0], SPConfig.parse(mode));
505     mcom.reopen(ports[1], SPConfig.parse(mode));
506     scom.flush();
507     mcom.flush();
508 
509     scom.config = mcom.config;
510 
511     scom.readTimeout = 1000.msecs;
512     mcom.writeTimeout = 100.msecs;
513 
514     version (OSX) enum BS = BUFFER_SIZE / 2;
515     else          enum BS = BUFFER_SIZE * 4;
516 
517     auto slave  = new CFSlave(scom,  BS);
518     auto master = new CFMaster(mcom, BS);
519 
520     void run()
521     {
522         bool work = true;
523         int step;
524         alias TERM = Fiber.State.TERM;
525         while (work)
526         {
527             if (master.state != TERM) master.call;
528             Thread.sleep(5.msecs);
529             if (slave.state != TERM) slave.call;
530 
531             step++;
532             if (master.state == TERM && slave.state == TERM)
533             {
534                 assert(slave.result.length == master.data.length);
535                 import std.algorithm : equal;
536                 enforce(equal(cast(ubyte[])slave.result, cast(ubyte[])master.data));
537                 work = false;
538                 testPrint("basic loop steps: ", step);
539             }
540         }
541     }
542 
543     run();
544 }
545 
546 void readTimeoutTest(string[2] ports)
547 {
548     void[1024] buffer = void;
549 
550     auto comA = new SerialPortFR(ports[0], 19_200);
551     scope (exit) comA.close();
552     comA.flush();
553     assertThrown!TimeoutException(comA.readContinues(buffer[], 1.msecs, 1.msecs));
554     assertNotThrown!TimeoutException(comA.readContinues(buffer[], 1.msecs, 1.msecs, false));
555     assertThrown!TimeoutException(comA.read(buffer[]));
556     assertThrown!TimeoutException(comA.read(buffer[], comA.CanRead.anyNonZero));
557 
558     auto comB = new SerialPortBlk(ports[1], 19_200, "8N1");
559     scope (exit) comB.close();
560     comB.flush();
561     comB.readTimeout = 1.msecs;
562     assertThrown!TimeoutException(comB.read(buffer[]));
563     assertThrown!TimeoutException(comB.read(buffer[], comB.CanRead.anyNonZero));
564 }
565 
566 void readTimeoutTestConfig(SP : SerialPort)(string[2] ports, SerialPort.CanRead cr)
567 {
568     enum mode = "38400:8N1";
569 
570     enum FULL = 100;
571     enum SEND = "helloworld";
572 
573     static void thfunc(string port)
574     {
575         auto com = new SP(port, mode);
576         com.flush();
577         scope (exit) com.close();
578         com.write(SEND);
579     }
580 
581     auto com = new SP(ports[0], mode);
582     scope (exit) com.close();
583     auto rt = 300.msecs;
584     com.readTimeout = rt;
585     com.flush();
586     assert(com.readTimeout == rt);
587 
588     void[FULL] buffer = void;
589     void[] data;
590 
591     spawnLinked(&thfunc, ports[1]);
592 
593     Thread.sleep(rt);
594 
595     if (cr == SerialPort.CanRead.anyNonZero)
596     {
597         assertNotThrown(data = com.read(buffer, cr));
598         assert(cast(string)data == SEND);
599         assertThrown!TimeoutException(data = com.read(buffer, cr));
600     }
601     else if (cr == SerialPort.CanRead.allOrNothing)
602         assertThrown!TimeoutException(data = com.read(buffer));
603     else if (cr == SerialPort.CanRead.zero)
604     {
605         assertNotThrown(data = com.read(buffer, cr));
606         assertNotThrown(data = com.read(buffer, cr));
607         assertNotThrown(data = com.read(buffer, cr));
608     }
609     else assert(0, "not tested variant of CanRead");
610 
611     receive((LinkTerminated e) { });
612 }
613 
614 void readTimeoutTestConfig2(SP : SerialPort)(string[2] ports, SerialPort.CanRead cr)
615 {
616     enum mode = "38400:8N1";
617 
618     static void thfunc(string port)
619     {
620         auto com = new SP(port, mode);
621         scope (exit) com.close();
622         com.flush();
623         Thread.sleep(200.msecs);
624         com.write("one");
625         Thread.sleep(200.msecs);
626         com.write("two");
627     }
628 
629     auto com = new SP(ports[0], mode);
630     scope (exit) com.close();
631     com.readTimeout = cr == SerialPort.CanRead.zero ? 10.msecs : 300.msecs;
632     com.flush();
633 
634     void[6] buffer = void;
635     void[] data;
636 
637     spawnLinked(&thfunc, ports[1]);
638 
639     if (cr == SerialPort.CanRead.anyNonZero)
640     {
641         assertNotThrown(data = com.read(buffer, cr));
642         assert(cast(string)data == "one");
643         assertNotThrown(data = com.read(buffer, cr));
644         assert(cast(string)data == "two");
645     }
646     else if (cr == SerialPort.CanRead.allOrNothing)
647         assertThrown!TimeoutException(data = com.read(buffer));
648     else if (cr == SerialPort.CanRead.zero)
649     {
650         assertNotThrown(data = com.read(buffer, cr));
651         assert(cast(string)data == "");
652         Thread.sleep(300.msecs);
653         assertNotThrown(data = com.read(buffer, cr));
654         assert(cast(string)data == "one");
655         assertNotThrown(data = com.read(buffer, cr));
656         assert(cast(string)data == "");
657         Thread.sleep(200.msecs);
658         assertNotThrown(data = com.read(buffer, cr));
659         assert(cast(string)data == "two");
660         assertNotThrown(data = com.read(buffer, cr));
661         assert(cast(string)data == "");
662     }
663     else assert(0, "not tested variant of CanRead");
664 
665     receive((LinkTerminated e) { });
666 }
667 
668 void fiberSleepFuncTest(string[2] ports)
669 {
670     import std.datetime.stopwatch : StopWatch, AutoStart;
671 
672     static void sf(Duration d) @nogc
673     {
674         const sw = StopWatch(AutoStart.yes);
675         if (auto f = Fiber.getThis)
676             while (sw.peek < d) f.yield();
677         else Thread.sleep(d);
678     }
679 
680     CFMaster master;
681 
682     size_t sf2_cnt;
683     void sf2(Duration d) @nogc
684     {
685         const sw = StopWatch(AutoStart.yes);
686         if (auto f = Fiber.getThis)
687             while (sw.peek < d)
688             {
689                 master.yield();
690                 sf2_cnt++;
691             }
692         else Thread.sleep(d);
693     }
694 
695     auto slave = new CFSlave(new SerialPortFR(ports[0], &sf), BUFFER_SIZE);
696     scope (exit) slave.com.close();
697     master = new CFMaster(new SerialPortFR(ports[1], &sf2), BUFFER_SIZE);
698     scope (exit) master.com.close();
699 
700     bool work = true;
701     int step;
702     while (work)
703     {
704         alias TERM = Fiber.State.TERM;
705         if (master.state != TERM) master.call;
706         if (slave.state != TERM) slave.call;
707 
708         step++;
709         Thread.sleep(30.msecs);
710         if (master.state == TERM && slave.state == TERM)
711         {
712             if (slave.result.length == master.data.length)
713             {
714                 import std.algorithm : equal;
715                 enforce(equal(cast(ubyte[])slave.result, cast(ubyte[])master.data));
716                 work = false;
717                 testPrint("basic loop steps: ", step);
718             }
719             else throw new Exception(text(slave.result, " != ", master.data));
720         }
721     }
722 }