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 }