1 
2 //          Copyright Ferdinand Majerech 2011 - 2012.
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          http://www.boost.org/LICENSE_1_0.txt)
6 
7 
8 module dgamevfs.test;
9 
10 
11 import dgamevfs._;
12 
13 
14 import std.algorithm;
15 import std.file;
16 import std.stdio;
17 import std.traits;
18 import std.typecons;
19 
20 
21 //collectException has some kind of bug (probably lazy expressions),
22 //so using this instead:
23 bool fail(D)(D dg) if(isDelegate!D)
24 {
25     try{dg();}
26     catch(VFSException e){return true;}
27     return false;
28 }
29 
30 bool testVFSFileInfo(VFSDir parent, string name, 
31                        Flag!"exists" exists, Flag!"writable" writable)
32 {
33     auto file = parent.file(name);
34     if(file.path != parent.path ~ "/" ~ name)             {return false;}
35     if(file.exists != exists || file.writable != writable){return false;}
36 
37     if(!exists && (!fail(&file.bytes) || !fail(&file.input))){return false;}
38     if(exists && (fail(&file.bytes) || fail(&file.input)))   {return false;}
39     return true;
40 }
41 
42 bool testVFSFileRead(VFSDir parent, string name, string expected)
43 {
44     auto file = parent.file(name);
45 
46     VFSFileInput refcountTestIn(VFSFileInput i){return i;}
47     //read what we've written before
48     {
49         auto i = refcountTestIn(file.input);
50         auto buf = new char[expected.length * 2];
51         auto slice = i.read(cast(void[])buf);
52         if(slice.length != file.bytes || cast(string)slice != expected)
53         {
54             return false;
55         }
56     }
57     //should be automatically closed
58     if(file.open){return false;}
59 
60     return true;
61 }
62 
63 bool testVFSFileIO(VFSDir parent, string name, string data)
64 {
65     auto file = parent.file(name);
66 
67     VFSFileOutput refcountTestOut(VFSFileOutput o){return o;}
68     //write, this should create the file if it doesn't exist yet
69     {
70         auto o = refcountTestOut(file.output);
71         o.write(cast(const void[])data);
72     }
73     //should be automatically closed
74     if(file.open){return false;}
75 
76     if(fail(&file.bytes) || file.bytes != data.length || fail(&file.input))
77     {
78         return false;
79     }
80 
81     if(!testVFSFileRead(parent, name, data))
82     {
83         return false;
84     }
85 
86     return true;
87 }
88 
89 bool testVFSFileSeek(VFSDir parent, string name)
90 {
91     auto file = parent.file(name);
92     {
93         auto output = file.output;
94         output.write(cast(const void[])"Teh smdert iz");
95         if(!fail({output.seek(-4, Seek.Set);}) || 
96            !fail({output.seek(4, Seek.End);}))
97         {
98             return false;
99         }
100 
101         output.seek(0, Seek.Set);
102         output.write(cast(const void[])"The");
103         output.write(cast(const void[])" answaa");
104         output.seek(-2, Seek.Current);
105         output.write(cast(const void[])"er");
106         output.seek(-2, Seek.End);
107         output.write(cast(const void[])"is 42.");
108     }
109     {
110         auto input = file.input;
111         if(!fail({input.seek(-4, Seek.Set);}) || 
112            !fail({input.seek(4, Seek.End);}))
113         {
114             return false;
115         }
116         
117         auto buffer = new char[3];
118         input.read(cast(void[])buffer);
119         if(buffer != "The"){return false;}
120 
121         input.seek(-3, Seek.End);
122         input.read(cast(void[])buffer);
123         if(buffer != "42."){return false;}
124 
125         input.seek(0, Seek.Set);
126         buffer = new char[cast(uint)file.bytes];
127         input.read(cast(void[])buffer);
128         if(buffer != "The answer is 42."){return false;}
129     }
130     return true;
131 }
132 
133 bool testVFSDirInfo(VFSDir parent, string name, Flag!"exists" exists,
134                       Flag!"writable" writable, Flag!"create" create)
135 {
136     auto dir = parent.dir(name);
137     if(dir.path != parent.path ~ "/" ~ name)            {return false;}
138     if(dir.exists != exists || dir.writable != writable){return false;}
139     if(!create){return true;}
140     dir.create();
141     if(!dir.exists){return false;}
142     return true;
143 }
144 
145 bool testVFSDirFiles(VFSDir dir, Flag!"deep" deep)
146 {
147     foreach(file; dir.files(deep))
148     {
149         auto sepsAdded = count(file.path, '/') - count(dir.path, '/');
150         if(sepsAdded < 1){return false;} 
151         if(!file.exists){return false;}
152     }
153     return true;
154 }
155 
156 bool testVFSDirDirs(VFSDir dir, Flag!"deep" deep)
157 {
158     foreach(sub; dir.dirs(deep))
159     {
160         auto sepsAdded = count(sub.path, '/') - count(dir.path, '/');
161         if(sepsAdded < 1){return false;} 
162         if(!sub.exists){return false;}
163     }
164     return true;
165 }
166 
167 bool testVFSDirContents(VFSDir dir, string[] expectedFiles, string[] expectedDirs, string glob = null)
168 {
169     alias std.algorithm.remove remove;
170     foreach(file; dir.files(Yes.deep, glob))
171     {
172         if(!canFind(expectedFiles, file.path))
173         {
174             writeln("FAILED file iteration (unexpected file ", file.path, ")");
175             //Unexpected file.
176             return false;
177         }
178         expectedFiles = remove!((string a){return a == file.path;})(expectedFiles);
179     }
180     foreach(sub; dir.dirs(Yes.deep, glob))
181     {
182         if(!canFind(expectedDirs, sub.path))
183         {
184             writeln("FAILED directory iteration (unexpected directory ", sub.path, ")");
185             //Unexpected directory.
186             return false;
187         }
188         expectedDirs = remove!((string a){return a == sub.path;})(expectedDirs);
189     }
190     if(expectedFiles.length > 0 || expectedDirs.length > 0)
191     {
192         writeln("FAILED file/directory iteration (unexpected files, dirs: ", 
193                  expectedFiles, expectedDirs, ")");
194         //Missing file/directory.
195         return false;
196     }
197     return true;
198 }
199 
200 bool testVFSDirGlob(VFSDir root)
201 {
202     assert(root.path == "root");
203     auto dirsCreate = ["fonts",
204                        "fonts/ttf",
205                        "fonts/otf",
206                        "fonts/extra",
207                        "shaders",
208                        "shaders/extra",
209                        "txt"];
210     auto filesCreate = ["fonts/config.txt",
211                         "fonts/ttf/a.ttf",
212                         "fonts/ttf/info.txt",
213                         "fonts/ttf/b.ttf",
214                         "fonts/otf/a.otf",
215                         "fonts/otf/otf.txt",
216                         "fonts/extra/bitmapfont.png",
217                         "shaders/extra/info.txt",
218                         "shaders/extra/effect.frag",
219                         "shaders/extra/effect.vert"];
220     with(root)
221     {
222         foreach(subdir; dirsCreate)
223         {
224             dir(subdir).create();
225         }
226         foreach(subfile; filesCreate)
227         {
228             file(subfile).output.write(cast(const void[])"42"); 
229         }
230 
231         //Containing font:
232         auto fontDirs   = ["root/fonts", 
233                            "root/fonts/ttf", 
234                            "root/fonts/otf", 
235                            "root/fonts/extra"];
236         auto fontFiles  = ["root/fonts/config.txt",
237                            "root/fonts/ttf/a.ttf",
238                            "root/fonts/ttf/info.txt",
239                            "root/fonts/ttf/b.ttf",
240                            "root/fonts/otf/a.otf",
241                            "root/fonts/otf/otf.txt",
242                            "root/fonts/extra/bitmapfont.png"];
243 
244         //Containing extra:
245         auto extraDirs  = ["root/fonts/extra", "root/shaders/extra"];
246         auto extraFiles = ["root/fonts/extra/bitmapfont.png",
247                            "root/shaders/extra/info.txt",
248                            "root/shaders/extra/effect.frag",
249                            "root/shaders/extra/effect.vert"];
250 
251         //Ending with ttf:
252         auto ttfDirs    = ["root/fonts/ttf"];
253         auto ttfFiles   = ["root/fonts/ttf/a.ttf", "root/fonts/ttf/b.ttf"];
254 
255         //Ending with .txt:
256         auto txtDirs    = cast(string[])[];
257         auto txtFiles   = ["root/fonts/config.txt", 
258                            "root/fonts/ttf/info.txt", 
259                            "root/fonts/otf/otf.txt",
260                            "root/shaders/extra/info.txt"];
261 
262         //Containing tf:
263         auto tfDirs     = ["root/fonts/ttf", "root/fonts/otf"];
264         auto tfFiles    = ["root/fonts/ttf/a.ttf",
265                            "root/fonts/ttf/info.txt",
266                            "root/fonts/ttf/b.ttf",
267                            "root/fonts/otf/a.otf",
268                            "root/fonts/otf/otf.txt"];
269 
270         if(!testVFSDirContents(root, fontFiles,  fontDirs,  "*font*") ||
271            !testVFSDirContents(root, extraFiles, extraDirs, "*extra*") ||
272            !testVFSDirContents(root, ttfFiles,   ttfDirs,   "*ttf") ||
273            !testVFSDirContents(root, txtFiles,   txtDirs,   "*.txt") ||
274            !testVFSDirContents(root, tfFiles,    tfDirs,    "*tf*"))
275         {
276             return false;
277         }
278     }
279 
280     return true;
281 }
282 
283 bool testVFSDirMain(VFSDir root)
284 {
285     //We expect these to be true from the start.
286     if(!root.exists || !root.writable || root.path != "root"){return false;}
287 
288     string answer = "The answer is 42.";
289 
290     //Nonexistent file:
291     if(!testVFSFileInfo(root, "file1", No.exists, Yes.writable) || 
292        !testVFSFileIO(root, "file1", answer))
293     {
294         writeln("FAILED nonexistent file");
295         return false;
296     }
297 
298     //Direct file access: (file1 exists now)
299     if(!testVFSFileInfo(root, "file1", Yes.exists, Yes.writable) ||
300        !testVFSFileRead(root, "file1", answer))
301     {
302         writeln("FAILED existing file");
303         return false;
304     }
305 
306     //Invalid file paths:
307     if(!fail({root.file("a::c");}) || !fail({root.file("nonexistent_subdir/file");}))
308     {
309         writeln("FAILED invalid file paths");
310         return false;
311     }
312 
313     //Creating subdirectories:
314     if(!testVFSDirInfo(root, "sub1", No.exists, Yes.writable, Yes.create) ||
315        !testVFSDirInfo(root, "sub1/sub2", No.exists, Yes.writable, Yes.create))
316     {
317         writeln("FAILED creating subdirectories");
318         return false;
319     }
320     foreach(char c; 'A' .. 'Z')
321     {
322         auto name = "sub1/sub2/sub" ~ c;
323         if(!testVFSDirInfo(root, name, No.exists, Yes.writable, Yes.create))
324         {
325             writeln("FAILED creating many subdirectories");
326             return false;
327         }
328         auto sub = root.dir(name);
329         foreach(char f; '1' .. '9')
330         {
331             auto fname = "file" ~ f;
332             if(!testVFSFileInfo(sub, fname, No.exists, Yes.writable) || 
333                !testVFSFileIO(sub, fname, answer))
334             {
335                 writeln("FAILED creating files in subdirectories");
336                 return false;
337             }        
338         }
339     }
340 
341     //File in a subdirectory:
342     if(!testVFSFileInfo(root, "sub1/sub2/subN/file5", Yes.exists, Yes.writable) ||
343        !testVFSFileIO(root, "sub1/sub2/subN/file5", answer))
344     {
345         writeln("FAILED file in a subdirectory");
346         return false;
347     }
348 
349     //Seeking:
350     if(!testVFSFileSeek(root, "seek_test"))
351     {
352         writeln("FAILED file seeking");
353         return false;
354     }
355 
356     //Subdirectory:
357     {
358         auto sub = root.dir("subdir");
359         //sub doesn't exist yet:
360         if(!fail({sub.file("file");}))
361         {
362             writeln("FAILED subdirectory file/dir access");
363             return false;
364         }
365         sub.create();
366         //Now it exists:
367         if(fail({sub.file("file");}))
368         {
369             writeln("FAILED subdirectory file/dir access");
370             return false;
371         }
372         //Looking for a file in a subdir of sub that doesn't exist:
373         if(!fail({sub.file("subdir2/file");}))
374         {
375             writeln("FAILED subdirectory file/dir access");
376             return false;
377         }
378     }
379 
380     //files()/dirs():
381     if(!testVFSDirFiles(root, No.deep) || !testVFSDirFiles(root, Yes.deep) ||
382        !testVFSDirDirs(root, No.deep)  || !testVFSDirDirs(root, Yes.deep))
383     {
384         writeln("FAILED file/directory iteration");
385         return false;
386     }
387 
388     //Globbing:
389     if(!testVFSDirGlob(root))
390     {
391         writeln("FAILED globbing");
392         return false;
393     }
394  
395     //created before
396     auto sub = root.dir("sub1");
397     //files()/dirs() from a subdir:
398     if(!testVFSDirFiles(sub, No.deep) || !testVFSDirFiles(sub, Yes.deep) ||
399        !testVFSDirDirs(sub, No.deep)  || !testVFSDirDirs(sub, Yes.deep))
400     {
401         writeln("FAILED file/directory iteration in a subdirectory");
402         return false;
403     }
404 
405     //We added some files to subdirs, so files().length should be less that files(Yes.deep).length:
406     if(root.files().length >= root.files(Yes.deep).length || 
407        root.dirs().length >= root.dirs(Yes.deep).length)
408     {
409         writeln("FAILED file/directory iteration item count");
410         return false;
411     }
412 
413     //Nonexistent dir:
414     {
415         auto dir = root.dir("nonexistent");
416         if(!fail({dir.file("file");}) ||
417            !fail({dir.dir("dir");})   ||
418            !fail({dir.files();})      ||
419            !fail({dir.dirs();}))
420         {
421             writeln("FAILED nonexistent directory");
422             return false;
423         }
424     }
425 
426     return true;
427 }
428 
429 bool testMemoryDir()
430 {
431     auto memoryDir = new MemoryDir("root");
432     //basic info methods
433     if(!memoryDir.writable || memoryDir.exists ||
434        memoryDir.path != memoryDir.name || memoryDir.path != "root")
435     {
436         writeln("FAILED MemoryDir info");
437         return false;
438     }
439 
440     //create
441     memoryDir.create();
442     if(!memoryDir.exists)
443     {
444         writeln("FAILED MemoryDir create");
445         return false;
446     }
447 
448     if(!testVFSDirMain(memoryDir))
449     {
450         writeln("FAILED MemoryDir general");
451         return false;
452     }
453 
454     //remove
455     memoryDir.dir("subdir").create();
456     memoryDir.remove();
457     if(memoryDir.exists)
458     {
459         writeln("FAILED memoryDir remove");
460         return false;
461     }
462     return true;
463 }
464 
465 bool testStackDirMount()
466 {
467     auto cycle1 = new StackDir("cycle1");
468     auto cycle2 = new StackDir("cycle2");
469     cycle1.mount(cycle2);
470     if(!fail({cycle2.mount(cycle1);}))
471     {
472         writeln("FAILED circular mounting");
473         return false;
474     }
475 
476     auto parent = new StackDir("parent");
477     auto child  = new MemoryDir("child");
478     auto main   = new StackDir("main");
479     parent.mount(child);
480     if(fail({main.mount(child);}))
481     {
482         writeln("FAILED mounting a dir with parent");
483         return false;
484     }
485 
486     auto child2 = new StackDir("child");
487     if(!fail({parent.mount(child2);}))
488     {
489         writeln("FAILED mounting a dir with same name");
490         return false;
491     } 
492 
493     return true;
494 }
495 
496 bool testStackDirStacking()
497 {
498     //Initialize directory structure:
499     auto mainPkg = new MemoryDir("main");
500     with(mainPkg)
501     {
502         create();
503         dir("fonts").create();
504         dir("fonts/ttf").create();
505         dir("fonts/otf").create();
506         file("fonts/ttf/a.ttf").output.write(cast(const void[])"42");
507         file("fonts/otf/b.otf").output.write(cast(const void[])"42");
508         dir("logs").create();
509         dir("shaders").create();
510         file("shaders/font.vert").output.write(cast(const void[])"42");
511         file("shaders/font.frag").output.write(cast(const void[])"42");
512         writable = false;
513     }
514 
515     auto userPkg = new MemoryDir("user");
516     with(userPkg)
517     {
518         create();
519         dir("fonts").create();
520         dir("fonts/ttf").create();
521         file("fonts/ttf/a.ttf").output.write(cast(const void[])"42");
522         dir("logs").create();
523         dir("shaders").create();
524         dir("maps").create();
525     }
526 
527     auto modPkg = new MemoryDir("mod");
528     auto modLog = "mod/logs/memory.log";
529     with(modPkg)
530     {
531         create();
532         dir("logs").create();
533         dir("shaders").create();
534         file("shaders/font.vert").output.write(cast(const void[])"42");
535         file("logs/memory.log").output.write(cast(const void[])modLog);
536         writable = false;
537     }
538 
539     auto stackDir = new StackDir("root");
540     stackDir.mount(mainPkg);
541     stackDir.mount(userPkg);
542     stackDir.mount(modPkg);
543 
544     if(!stackDir.writable || !stackDir.exists)
545     {
546         writeln("FAILED stackdir info");
547         return false;
548     }
549 
550     //File/dir access:
551     {
552         //Should find the right file to get bytes() and read from:
553         if(!testVFSFileRead(stackDir.dir("logs"), "memory.log", modLog))
554         {
555             writeln("FAILED stackdir read");
556             return false;
557         }
558         //This should write to the "user" package.
559         stackDir.dir("logs").file("memory.log").output.write(cast(const void[])"out");
560         //This should read from the "mod" package
561         if(!testVFSFileRead(stackDir.dir("logs"), "memory.log", modLog))
562         {
563             writeln("FAILED stackdir read shadowing written");
564             return false;
565         }
566         if(!testVFSFileRead(stackDir.dir("user::logs"), "memory.log", "out"))
567         {
568             writeln("FAILED stackdir explicitly read written");
569             return false;
570         }
571     }
572 
573     //File/dir iteration:
574     auto expectedFiles = ["root/logs/memory.log",
575                            "root/shaders/font.vert",
576                            "root/shaders/font.frag",
577                            "root/fonts/ttf/a.ttf",
578                            "root/fonts/otf/b.otf"];
579     auto expectedDirs  = ["root/logs",
580                            "root/shaders",
581                            "root/maps",
582                            "root/fonts",
583                            "root/fonts/ttf",
584                            "root/fonts/otf"];
585     if(!testVFSDirContents(stackDir, expectedFiles, expectedDirs))
586     {
587             writeln("FAILED file/directory iteration");
588             return false;
589     }
590 
591     expectedFiles = ["root/fonts/ttf/a.ttf",
592                       "root/fonts/otf/b.otf"];
593     expectedDirs =  ["root/fonts/ttf",
594                       "root/fonts/otf"];
595     if(!testVFSDirContents(stackDir.dir("fonts"), expectedFiles, expectedDirs))
596     {
597             writeln("FAILED file/directory iteration in a subdirectory");
598             return false;
599     }
600 
601     return true;
602 }
603 
604 bool testStackDir()
605 {
606     //General:
607     {
608         auto mainPkg = new MemoryDir("main");
609         auto userPkg = new MemoryDir("user");
610         auto stackDir = new StackDir("root");
611         stackDir.mount(mainPkg);
612         stackDir.mount(userPkg);
613 
614         //basic info methods
615         if(!stackDir.writable || stackDir.exists ||
616            stackDir.path != stackDir.name || stackDir.path != "root")
617         {
618             writeln("FAILED StackDir info");
619             return false;
620         }
621 
622         //create
623         stackDir.create();
624         if(!stackDir.exists)
625         {
626             writeln("FAILED StackDir create");
627             return false;
628         }
629 
630         if(!testVFSDirMain(stackDir))
631         {
632             writeln("FAILED StackDir general");
633             return false;
634         }
635 
636         //remove
637         stackDir.dir("subdir").create();
638         stackDir.remove();
639         if(stackDir.exists)
640         {
641             writeln("FAILED stackDir remove");
642             return false;
643         }
644     }
645 
646     //Mounting:
647     if(!testStackDirMount())
648     {
649         writeln("FAILED StackDir mount");
650         return false;
651     }
652 
653     //Stacking:
654     if(!testStackDirStacking())
655     {
656         writeln("FAILED StackDir stacking");
657         return false;
658     }
659 
660     return true;
661 }
662 
663 bool testFSDir()
664 {
665     auto path = "testFSDir";
666     if(exists(path)){rmdirRecurse(path);}
667     mkdir(path);
668     scope(exit){rmdirRecurse(path);}
669 
670     auto fsDir = new FSDir("root", path ~ "/root", Yes.writable);
671     //basic info methods
672     if(!fsDir.writable || fsDir.exists ||
673        fsDir.path != fsDir.name || fsDir.path != "root")
674     {
675         writeln(cast(bool)fsDir.writable, " ", fsDir.exists, " ", 
676                 fsDir.path, " ", fsDir.name);
677         writeln("FAILED FSDir info");
678         return false;
679     }
680 
681     //create
682     fsDir.create();
683     if(!fsDir.exists)
684     {
685         writeln("FAILED FSDir create");
686         return false;
687     }
688 
689     if(!testVFSDirMain(fsDir))
690     {
691         writeln("FAILED FSDir general");
692         return false;
693     }
694 
695     //remove
696     fsDir.dir("subdir").create();
697     fsDir.remove();
698     if(fsDir.exists)
699     {
700         writeln("FAILED FSDir remove");
701         return false;
702     }
703 
704     return true;
705 }
706 
707 unittest
708 {
709     writeln("---------- ",
710             testMemoryDir() ? "SUCCESS" : "FAILURE",
711             " MemoryDir unittest ", "----------");
712     writeln("---------- ",
713             testStackDir() ? "SUCCESS" : "FAILURE",
714             " StackDir unittest ", "----------");
715     writeln("---------- ",
716             testFSDir() ? "SUCCESS" : "FAILURE",
717             " FSDir unittest ", "----------");
718 }