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 }