1 /** Contains the functions that will be accessible in bluejay scripts. */ 2 module bluejay.functions; 3 4 import luad.all; 5 import tested : test = name; 6 7 /** Return values from executing a process. 8 9 This must remain at the top-level to avoid access violations. 10 */ 11 struct ExecuteReturns { 12 int ReturnCode; 13 string Output; 14 15 @safe pure nothrow @nogc 16 this(int r, string o) { ReturnCode = r; Output = o; } 17 } 18 19 /** Functions to run tests. */ 20 class TestFunctions { 21 import bluejay.execution_state : Options; 22 private Options __options; 23 private LuaState __lua; 24 25 @safe pure nothrow @nogc 26 this(ref LuaState lua, Options options) { 27 __options = options; 28 __lua = lua; 29 } 30 31 // Optional parameter: args. 32 @safe 33 auto run(string command, string[] args...) const { 34 import std.process : executeShell; 35 if (args.length > 1) 36 throw new Exception("Too many arguments given for args."); 37 38 string arg; 39 if (args.length == 1) arg = args[0]; 40 41 auto output = executeShell(command ~ " " ~ arg); 42 return ExecuteReturns(output.status, output.output); 43 } 44 45 @test("TestFunctions.run executes a file.") 46 unittest { 47 import std..string : strip; 48 auto lua = new LuaState(); 49 auto t = new TestFunctions(lua, Options()); 50 51 void func() @safe { 52 auto ret = t.run("echo", "asdf"); 53 assert(ret.Output.strip == "asdf"); 54 assert(ret.ReturnCode == 0); 55 } func(); 56 } 57 58 // Optional parameter: args. 59 auto spawn(string command, string[] args...) const { 60 import std.process : spawnShell; 61 if (args.length > 1) 62 throw new Exception("Too many arguments given for args."); 63 64 string arg; 65 if (args.length == 1) arg = args[0]; 66 return spawnShell(command ~ " " ~ arg).processID; 67 } 68 69 @test("TestFunctions.spawn executes a file and returns its PID.") 70 unittest { 71 import std..string : strip; 72 auto lua = new LuaState(); 73 auto t = new TestFunctions(lua, Options()); 74 auto ret = t.spawn("echo", "asdf"); 75 assert(ret > 0); 76 } 77 78 /** Compare the contents of two files. 79 80 This works like the UNIX diff command; we return true if the files are 81 the same, and false otherwise. 82 83 Differences in line endings are ignored. 84 */ 85 bool diff(string path1, string path2) const { 86 import std.stdio : File, Yes; 87 auto f1 = File(path1); 88 scope(exit) f1.close; 89 auto f2 = File(path2); 90 scope(exit) f2.close; 91 92 auto f2range = f2.byLineCopy(Yes.keepTerminator); 93 foreach (f1line; f1.byLineCopy(Yes.keepTerminator)) { 94 if (f2range.empty) return false; 95 if (! __diffString(f1line, f2range.front)) return false; 96 f2range.popFront; 97 } 98 return true; 99 } 100 101 @safe pure nothrow @nogc 102 private bool __diffString(string one, string two) const { 103 if (one == two) return true; 104 105 if (one[$-2] == '\r') { 106 if (one[0 .. $-2] == two[0 .. $-1]) return true; 107 } else if (two[$-2] == '\r') { 108 if (two[0 .. $-2] == one[0 .. $-1]) return true; 109 } 110 return false; 111 } 112 113 @test("TestFunctions.diff is newline-agnostic.") 114 unittest { 115 auto lua = new LuaState(); 116 auto t = new TestFunctions(lua, Options()); 117 string one = " asdfsdfg\n"; 118 string two = " asdfsdfg\r\n"; 119 120 @safe pure nothrow @nogc func() { 121 assert(t.__diffString(one, two), 122 "diff says the second string is different."); 123 assert(t.__diffString(two, one), 124 "diff says the first string is different."); 125 } func(); 126 } 127 128 @test("TestFunctions.diff returns true on identical strings.") 129 unittest { 130 auto lua = new LuaState(); 131 auto t = new TestFunctions(lua, Options()); 132 string one = " asdfsdfg\n"; 133 string two = " asdfsdfg\n"; 134 135 @safe pure nothrow @nogc func() { 136 assert(t.__diffString(one, two)); 137 assert(t.__diffString(two, one)); 138 } func(); 139 } 140 141 @test("TestFunctions.diff returns false on non-matching strings.") 142 unittest { 143 auto lua = new LuaState(); 144 auto t = new TestFunctions(lua, Options()); 145 string one = " asdfsdfg\n"; 146 string two = " asdfdfg\n"; 147 148 @safe pure nothrow @nogc func() { 149 assert(! t.__diffString(one, two)); 150 assert(! t.__diffString(two, one)); 151 } func(); 152 } 153 154 /** Return true if the provided code throws an error; false otherwise. 155 156 pcall takes care of Lua errors, and the try/catch handled D exceptions. 157 */ 158 nothrow 159 bool throws(string code) { 160 import luad.error : LuaErrorException; 161 try { 162 auto ret = __lua.doString("return pcall(" ~ __pcallFunc(code) 163 ~ ")")[0]; 164 return (! ret.to!bool); 165 } catch (LuaErrorException ex) { 166 return true; 167 } catch (Exception ex) { 168 return true; 169 } 170 assert(0); 171 } 172 173 @test("TestFunctions.throws returns true when Lua throws.") 174 unittest { 175 auto lua = new LuaState(); 176 auto t = new TestFunctions(lua, Options()); 177 178 void func() nothrow { 179 assert(t.throws("assert(true)")); 180 } func(); 181 } 182 183 @test("TestFunctions.throws returns false when Lua does not throw.") 184 unittest { 185 import bluejay.execution_state : ExecutionState; 186 auto lua = new ExecutionState(Options()); 187 auto t = new TestFunctions(lua, Options()); 188 189 void func() nothrow { 190 assert(! t.throws("getfenv()")); 191 } func(); 192 } 193 194 @test("Issue 1: TestFunctions.throws properly handles host function.") 195 unittest { 196 import bluejay.execution_state; 197 auto lua = new ExecutionState(Options()); 198 auto t = new TestFunctions(lua, Options()); 199 200 void func() nothrow { 201 assert(t.throws("Util:listDir('test/nonexistent')")); 202 } func(); 203 } 204 205 /** Convert a function call to arguments for the pcall function. */ 206 // TODO: This needs to be well-tested with error handling. 207 // Since we're testing for failure, we can't let this fail due to bad input. 208 @safe pure 209 private string __pcallFunc(string code) const { 210 import std.algorithm.searching : balancedParens, findSplit; 211 import std.algorithm.iteration : map, splitter; 212 import std.array : join; 213 import std..string : strip; 214 215 if (! balancedParens(code, '(', ')')) 216 throw new Exception("Missing parenthesis in function call."); 217 218 auto func = code.findSplit("("); 219 string args = func[2] 220 .splitter(',') 221 .map!(a => a.strip) 222 .join(','); 223 224 // Remove the outermost closing parenthesis. 225 if (args[$-1] != ')') 226 throw new Exception("Function is missing a closing parenthesis."); 227 228 args = args[0 .. $-1]; 229 if (args.length == 0) { 230 return func[0]; 231 } else return [func[0], args].join(','); 232 } 233 234 @test("TestFunctions.pcallFunc returns the currect pcall arguments with " ~ 235 "no parameters.") 236 unittest { 237 auto lua = new LuaState(); 238 auto t = new TestFunctions(lua, Options()); 239 240 void func() @safe pure { 241 auto ret = t.__pcallFunc("getfenv()"); 242 assert(ret == "getfenv"); 243 } func(); 244 } 245 246 @test("TestFunctions.pcallFunc returns the currect pcall arguments with " ~ 247 "one parameter.") 248 unittest { 249 auto lua = new LuaState(); 250 auto t = new TestFunctions(lua, Options()); 251 252 void func() @safe pure { 253 auto ret = t.__pcallFunc("print('some string')"); 254 assert(ret == "print,'some string'"); 255 } 256 } 257 258 @test("TestFunctions.pcallFunc returns the currect pcall arguments with " ~ 259 "two parameters.") 260 unittest { 261 auto lua = new LuaState(); 262 auto t = new TestFunctions(lua, Options()); 263 264 void func() @safe pure { 265 auto ret = t.__pcallFunc("print('some string', some_var)"); 266 assert(ret == "print,'some string',some_var"); 267 } 268 } 269 270 @test("Issue 1: TestFunctions.pcallFunc properly handles host function.") 271 unittest { 272 auto lua = new LuaState(); 273 auto t = new TestFunctions(lua, Options()); 274 275 void func() @safe pure { 276 auto ret = t.__pcallFunc("Util:listDir('test/nonexistent')"); 277 assert(ret == "Util:listDir,'test/nonexistent'"); 278 } 279 } 280 } 281 282 /** Generic helper functions. */ 283 struct UtilFunctions { 284 LuaState __lua; 285 286 @safe pure nothrow @nogc 287 this(ref LuaState lua) { 288 __lua = lua; 289 } 290 291 @safe pure 292 string strip(ref LuaObject self, string str) const { 293 import std..string : strip; 294 return str.strip; 295 } 296 297 @test("UtilFunctions.strip removes whitespace surrounding text.") 298 @safe 299 unittest { 300 auto u = UtilFunctions(); 301 auto l = LuaObject(); 302 303 void func() pure { 304 assert(u.strip(l, "\n asdf\t ") == "asdf"); 305 } func(); 306 } 307 308 @safe pure 309 string[] split(ref LuaObject self, string str) const { 310 import std..string : splitLines; 311 return str.splitLines; 312 } 313 314 @test("UtilFunctions.split properly splits a string by newlines.") 315 @safe 316 unittest { 317 auto l = LuaObject(); 318 auto u = UtilFunctions(); 319 auto str = "1 and\n2 and\n3 and\ndone."; 320 321 void func() pure { 322 auto ret = u.split(l, str); 323 324 assert(ret[0] == "1 and", "Failed on first line."); 325 assert(ret[1] == "2 and", "Failed on second line."); 326 assert(ret[2] == "3 and", "Failed on third line."); 327 assert(ret[3] == "done.", "Failed on fourth line."); 328 } func(); 329 } 330 331 string cwd() const { 332 import std.file : getcwd; 333 return getcwd(); 334 } 335 336 @test("UtilFunctions.cwd returns the application's current working directory.") 337 unittest { 338 import std.file : getcwd; 339 auto u = UtilFunctions(); 340 assert(u.cwd() == getcwd); 341 } 342 343 /+ Access violation. Other attempts have OutOfMemoryError and 344 MemoryOperationException. 345 346 // baseDir is the directory containing the script, which will generally be 347 // more useful than cwd(). 348 string baseDir() { 349 import std.path : dirName; 350 return __scriptDir.dirName; 351 } 352 353 @test("UtilFunctions.baseDir returns a directory name.") 354 unittest { 355 import std.path : isDir; 356 import bluejay.execution_state : ExecutionState, Options; 357 auto lua = new ExecutionState(Options(), "/some/dir"); 358 auto u = UtilFunctions(lua); 359 // Note that the actual value of baseDir is platform-specific. 360 // There's no reason to care about the actual value. 361 assert(u.baseDir().length > 0); 362 assert(u.baseDir().isDir, "The returned value is not a directory."); 363 } 364 +/ 365 366 // One optional param: filter. 367 string[] listDir(ref LuaObject self, string dir, string[] filter...) const { 368 import std.file; 369 370 if (filter.length > 1) 371 throw new Exception( 372 "Too many arguments passed to listDir(dir, [filter])."); 373 if (! dir.exists) 374 throw new Exception("The directory " ~ dir ~ " does not exist."); 375 string[] files; 376 377 if (dir.isFile) { 378 files ~= dir; 379 return files; 380 } 381 382 auto f = "*"; 383 if (filter.length == 1) { 384 f = filter[0]; 385 } 386 foreach (entry; dirEntries(dir, f, SpanMode.shallow)) { 387 files ~= entry; 388 } 389 return files; 390 } 391 392 @test("UtilFunctions.listDir returns directory listing without filter.") 393 unittest { 394 import std.array : array; 395 import std.file : getcwd, dirEntries, SpanMode; 396 auto l = LuaObject(); 397 auto u = UtilFunctions(); 398 auto dir = getcwd(); 399 string[] f = []; 400 assert(u.listDir(l, dir, f) == dirEntries(dir, SpanMode.shallow).array); 401 } 402 403 @test("UtilFunctions.listDir returns a filtered directory listing.") 404 unittest { 405 import std.array : array; 406 import std.file : getcwd, dirEntries, SpanMode; 407 auto l = LuaObject(); 408 auto u = UtilFunctions(); 409 auto dir = getcwd(); 410 string[] f = ["*.json"]; 411 assert(u.listDir(l, dir, f) == dirEntries(dir, f[0], SpanMode.shallow).array); 412 } 413 414 @safe pure nothrow 415 string fixPath(ref LuaObject self, string path) const { 416 import std.array : array; 417 import std.path : asNormalizedPath; 418 419 version(Windows) { 420 return path.asNormalizedPath.array; 421 } else { 422 import std.uni : isAlpha; 423 auto thePath = path.asNormalizedPath.array; 424 425 if (thePath[0].isAlpha) { 426 if (thePath[1] == ':') { 427 thePath = thePath[2 .. $]; 428 } 429 } 430 if (thePath[$-1] == '/' || thePath[$-1] == '\\') { 431 thePath = thePath[0 .. $-1]; 432 } 433 for (int i = 0; i < thePath.length; i++) { 434 if (thePath[i] == '\\') thePath[i] = '/'; 435 } 436 437 return thePath.idup; 438 } 439 } 440 441 version(Windows) { 442 @test("Windows: UtilFunctions.fixPath converts POSIX path to Windows " ~ 443 "path.") 444 unittest { 445 auto l = LuaObject(); 446 auto u = UtilFunctions(); 447 assert(u.fixPath(l, "/some/dir") == "\\some\\dir"); 448 assert(u.fixPath(l, "/some/dir/") == "\\some\\dir"); 449 } 450 } else version(Posix) { 451 @test("POSIX: UtilFunctions.fixPath converts Windows path to POSIX path.") 452 unittest { 453 auto l = LuaObject(); 454 auto u = UtilFunctions(); 455 assert(u.fixPath(l, "\\some\\dir") == "/some/dir"); 456 assert(u.fixPath(l, "\\some\\dir\\") == "/some/dir"); 457 assert(u.fixPath(l, "c:\\some\\dir") == "/some/dir"); 458 } 459 } 460 461 @safe 462 bool fileExists(ref LuaObject self, string path) const { 463 import std.file : exists, isFile; 464 return (path.exists && path.isFile); 465 } 466 467 @test("UtilFunctions.fileExists correctly reports whether a file exists.") 468 @safe 469 unittest { 470 auto l = LuaObject(); 471 auto u = UtilFunctions(); 472 assert(u.fileExists(l, "source/app.d")); 473 assert(! u.fileExists(l, "source/this-is-not-there.qwe")); 474 } 475 476 @safe 477 bool dirExists(ref LuaObject self, string path) const { 478 import std.file : exists, isDir; 479 return (path.exists && path.isDir); 480 } 481 482 @test("UtilFunctions.dirExists correctly reports whether a directory exists.") 483 @safe 484 unittest { 485 auto l = LuaObject(); 486 auto u = UtilFunctions(); 487 assert(u.dirExists(l, "source")); 488 assert(! u.dirExists(l, "nodirhere")); 489 } 490 491 /** Recursively deletes the specified directory. */ 492 bool removeDir(ref LuaObject self, string path) const { 493 import std.file : exists, isDir, rmdirRecurse; 494 if (path.exists && path.isDir) { 495 rmdirRecurse(path); 496 return ! path.exists; 497 } 498 return ! path.exists; 499 } 500 501 @test("UtilFunctions.removeDir correctly removes a directory.") 502 unittest { 503 import std.file : exists, isDir, mkdir, tempDir; 504 import std.path : dirSeparator; 505 506 immutable dirPath = tempDir() ~ dirSeparator ~ "util-removedir-this"; 507 mkdir(dirPath); 508 assert(dirPath.exists && dirPath.isDir, 509 "Failed to create a directory to test UtilFunction's removeDir."); 510 511 auto l = LuaObject(); 512 auto u = UtilFunctions(); 513 u.removeDir(l, dirPath); 514 assert(! dirPath.exists, "Failed to delete a directory."); 515 } 516 517 @test("UtilFunctions.removeDir on a nonexistent path does not throw.") 518 unittest { 519 import std.file : exists, tempDir; 520 import std.path : dirSeparator; 521 522 immutable dirPath = tempDir() ~ dirSeparator ~ "this-dir-is-not-here"; 523 assert(! dirPath.exists, 524 "A directory that should not exist is present. " ~ 525 "Cannot test UtilFunction's removeDir."); 526 527 auto l = LuaObject(); 528 auto u = UtilFunctions(); 529 u.removeDir(l, dirPath); 530 } 531 532 @safe nothrow 533 bool removeFile(ref LuaObject self, string path) const { 534 import std.file : exists, remove; 535 536 try { 537 if (path.exists) remove(path); 538 } catch (Exception) /* FileException */ {} 539 return (! path.exists); 540 } 541 542 @test("UtilFunctions.removeFile deletes a file and returns true.") 543 @safe 544 unittest { 545 import std.file : exists, isFile, tempDir, write; 546 import std.path : dirSeparator; 547 548 immutable filePath = tempDir() ~ dirSeparator ~ "util-removefile-this"; 549 filePath.write("a"); 550 assert(filePath.exists && filePath.isFile, 551 "Failed to create a file to test UtilFunction's removeDir."); 552 553 void func() nothrow { 554 auto l = LuaObject(); 555 auto u = UtilFunctions(); 556 assert(u.removeFile(l, filePath), "removeFile failed."); 557 } func(); 558 559 assert(! filePath.exists, "Failed to delete a file."); 560 } 561 562 @test("UtilFunctions.removeFile does not throw if the file doesn't exist.") 563 @safe 564 unittest { 565 import std.file : exists, isFile, tempDir; 566 567 immutable filePath = tempDir() ~ "this-file-should-not-be-here.txt"; 568 assert(! filePath.exists, 569 "A file that should not exist is present. Cannot test removeFile."); 570 void func() nothrow { 571 auto l = LuaObject(); 572 auto u = UtilFunctions(); 573 assert(u.removeFile(l, filePath), "removeFile failed."); 574 } func(); 575 } 576 577 void writeFile(ref LuaObject self, string path, string content) const { 578 import std.stdio : toFile; 579 content.toFile(path); 580 } 581 582 @test("UtilFunctions.writeFile writes text to the specified file.") 583 unittest { 584 auto l = LuaObject(); 585 auto u = UtilFunctions(); 586 auto f = u.__getName; 587 u.writeFile(l, f, "This is a test."); 588 589 import std.file : readText, remove; 590 string text = readText(f); 591 remove(f); 592 assert(text == "This is a test."); 593 } 594 595 @safe 596 string readFile(ref LuaObject self, string path) const { 597 import std.file : readText; 598 return readText(path); 599 } 600 601 @test("UtilFunctions.readFile reads the text of a file.") 602 @safe 603 unittest { 604 import std.file : write, remove; 605 606 auto l = LuaObject(); 607 auto u = UtilFunctions(); 608 auto f = u.__getName; 609 f.write("This is a test."); 610 auto text = u.readFile(l, f); 611 remove(f); 612 assert(text == "This is a test."); 613 } 614 615 @safe 616 void copyFile(ref LuaObject self, string source, string dest) const { 617 import std.file : copy, No; 618 copy(source, dest, No.preserveAttributes); 619 } 620 621 @test("UtilFunctions.copyFile copies a file.") 622 unittest { 623 import std.file : exists, tempDir, readText, write; 624 import std.path : dirSeparator; 625 auto l = LuaObject(); 626 auto u = UtilFunctions(); 627 auto src = tempDir ~ dirSeparator ~ u.__getName; 628 auto dest = tempDir ~ dirSeparator ~ u.__getName; 629 630 assert(! dest.exists, "The destination file already exists. " ~ 631 "Cannot test UtilFunctions.copyFile."); 632 633 src.write("some text"); 634 u.copyFile(l, src, dest); 635 assert(dest.exists, "Failed to copy the file."); 636 assert(dest.readText == "some text", "File improperly copied."); 637 } 638 639 /** Creates a directory in the system's temporary directory and returns 640 the path. 641 */ 642 // In DMD 2.075.0+ this can be @safe. 643 string getTempDir() const { 644 import std.file : exists, mkdirRecurse, tempDir; 645 import std.path : dirSeparator; 646 647 string dirName = ""; 648 while (true) { 649 dirName = tempDir() ~ dirSeparator ~ __getName(); 650 if (! dirName.exists) break; 651 } 652 653 mkdirRecurse(dirName); 654 return dirName ~ dirSeparator; 655 } 656 657 @test("UtilFunctions.getTempDir creates a temporary directory.") 658 unittest { 659 import std.file : exists, isDir, rmdirRecurse; 660 auto u = UtilFunctions(); 661 auto dir = u.getTempDir; 662 assert(dir.exists && dir.isDir); 663 664 rmdirRecurse(dir); 665 } 666 667 /** Creates a file in the system's temporary directory and returns the 668 path. 669 */ 670 @safe 671 string getTempFile() const { 672 import std.file : exists, tempDir, write; 673 import std.path : dirSeparator; 674 675 string fileName = ""; 676 while (true) { 677 fileName = tempDir() ~ dirSeparator ~ __getName() ~ ".tmp"; 678 if (! fileName.exists) break; 679 } 680 681 fileName.write(['\0']); 682 return fileName; 683 } 684 685 @test("UtilFunctions.getTempFile creates a temporary file.") 686 @safe 687 unittest { 688 import std.file : exists, isFile, remove; 689 auto u = UtilFunctions(); 690 auto f = u.getTempFile; 691 assert(f.exists && f.isFile); 692 693 remove(f); 694 } 695 696 @safe 697 private auto __getName() const { 698 import std.algorithm : fill; 699 import std.conv : to; 700 import std.random : Random, randomCover, unpredictableSeed; 701 702 enum dstring letters = "abcdefghijklmnopqrstuvwxyz"; 703 704 dchar[8] name; 705 fill(name[], randomCover(letters, Random(unpredictableSeed))); 706 return name.to!string(); 707 } 708 709 @test("UtilFunctions.__getName returns the name of a file that doesn't exist.") 710 @safe 711 unittest { 712 import std.file : exists; 713 auto u = UtilFunctions(); 714 assert(! exists(u.__getName)); 715 } 716 717 // We have an optional parameter for maxLength. 718 void pprint(ref LuaObject self, LuaObject obj, int[] params...) const { 719 if (params.length == 0) { 720 __pprint(self, obj, 4, 1); 721 } else if (params.length == 1) { 722 __pprint(self, obj, params[0], 1); 723 } else { 724 throw new Exception("Too many arguments were provided to pprint."); 725 } 726 } 727 728 private void __pprint(ref LuaObject self, LuaObject obj, int maxLevel, 729 int indent) const { 730 if (maxLevel == 0) return; 731 732 import std.stdio : write, writeln; 733 if (obj.typeName != "table") { 734 writeln(obj.toString); 735 return; 736 } 737 auto tbl = obj.to!LuaTable; 738 739 import std.range : repeat, take; 740 import std.conv : text; 741 auto spaces = ' '.repeat().take(indent*4).text; 742 foreach (LuaObject key, LuaObject val; tbl) { 743 if (val.typeName == "table") { 744 write(spaces, key, ":"); 745 if (maxLevel == 1) writeln(" {table}"); 746 else writeln(); 747 748 __pprint(self, val, maxLevel-1, indent+1); 749 } else { 750 writeln(spaces, key, ": ", val.toString); 751 } 752 } 753 } 754 } 755 756 /** Functions to manage the test script. */ 757 class ScriptFunctions { 758 private LuaState __lua; 759 760 @safe pure nothrow @nogc 761 this(ref LuaState lua) { 762 __lua = lua; 763 } 764 765 // One optional param: returnCode. 766 @safe 767 void exit(int[] params...) { 768 if (params.length > 1) 769 throw new Exception("Too many arguments passed to exit([return code])."); 770 int returnCode = params.length == 0 ? 0 : params[0]; 771 772 //import std.conv : text; 773 //import luad.c.all : luaopen_os; 774 //luaopen_os(__lua.state); 775 __lua.doString("cleanup() return"); 776 //__lua.doString("os.exit(" ~ returnCode.text ~ ")"); 777 } 778 }