1 /+dub.sdl:
2     name "test"
3     dependency "jcli" path="../"
4 +/
5 module test;
6 
7 import std, jcli;
8 
9 /++ DATA TYPES ++/
10 struct TestCase
11 {
12     string                  folder;
13     string                  params;
14     string[]                cleanupFiles;
15     Nullable!int            expectedStatus;
16     Nullable!(Regex!char)   outputRegex;
17     bool                    allowedToFail;
18 }
19 
20 struct TestCaseBuilder
21 {
22     TestCase testCase;
23 
24     TestCaseBuilder inFolder(string folder)
25     {
26         this.testCase.folder = folder;
27         return this;
28     }
29 
30     TestCaseBuilder withParams(string params)
31     {
32         this.testCase.params = params;
33         return this;
34     }
35 
36     TestCaseBuilder expectStatusToBe(int status)
37     {
38         this.testCase.expectedStatus = status;
39         return this;
40     }
41 
42     TestCaseBuilder expectOutputToMatch(string regexString)
43     {
44         this.testCase.outputRegex = regex(regexString);
45         return this;
46     }
47 
48     TestCaseBuilder cleanup(string fileName)
49     {
50         this.testCase.cleanupFiles ~= fileName;
51         return this;
52     }
53 
54     TestCaseBuilder allowToFail()
55     {
56         this.testCase.allowedToFail = true;
57         return this;
58     }
59 
60     TestCase finish()
61     {
62         return this.testCase;
63     }
64 }
65 TestCaseBuilder testCase(){ return TestCaseBuilder(); }
66 
67 struct TestResult
68 {
69     bool     passed;
70     string[] failedReasons;
71     TestCase testCase;
72 }
73 
74 /++ CONFIGURATION ++/
75 auto TEST_CASES = 
76 [
77     testCase().inFolder         ("./00-basic-usage-default-command/")
78               .withParams       ("20")
79               .expectStatusToBe (0)
80               .finish           (),
81     testCase().inFolder         ("./00-basic-usage-default-command/")
82               .withParams       ("20 --reverse")
83               .expectStatusToBe (128)
84               .finish           (),
85     
86     testCase().inFolder         ("./01-named-sub-commands/")
87               .withParams       ("return 0")
88               .expectStatusToBe (0)
89               .finish           (),
90     testCase().inFolder         ("./01-named-sub-commands/")
91               .withParams       ("r 128")
92               .expectStatusToBe (128)
93               .finish           (),
94 
95     testCase().inFolder         ("./02-shorthand-longhand-args/")
96               .withParams       ("return --code 0")
97               .expectStatusToBe (0)
98               .finish           (),
99     testCase().inFolder         ("./02-shorthand-longhand-args/")
100               .withParams       ("r -c=128")
101               .expectStatusToBe (128)
102               .finish           (),
103 
104     // Class inheritence is broken, but I don't really think I can fix it without compiler changes.
105     testCase().inFolder         ("./03-inheritence-base-commands/")
106               .withParams       ("add 1 2")
107               .expectStatusToBe (3)
108               .allowToFail      ()
109               .finish           (),
110     testCase().inFolder         ("./03-inheritence-base-commands/")
111               .withParams       ("add 1 2 --offset=7")
112               .expectStatusToBe (10)
113               .allowToFail      ()
114               .finish           (),
115 
116     testCase().inFolder         ("./04-custom-arg-binders/")
117               .withParams       ("./dub.sdl")
118               .expectStatusToBe (0)
119               .finish           (),
120     testCase().inFolder         ("./04-custom-arg-binders/")
121               .withParams       ("./lalaland.txt")
122               .expectStatusToBe (-1)
123               .finish           (),
124 
125     testCase().inFolder         ("./05-built-in-binders/")
126               .withParams       ("echo -b -i 2 -f 2.2 -s Hola -e red")
127               .expectStatusToBe (0)
128               .finish           (),
129 
130     testCase().inFolder             ("./08-arg-binder-validation")
131               .withParams           ("20 69")
132               .expectStatusToBe     (0)
133               .finish               (),
134     testCase().inFolder             ("./08-arg-binder-validation")
135               .withParams           ("69 69")
136               .expectStatusToBe     (-1)
137               .expectOutputToMatch  ("Expected number to be even")
138               .finish               (),
139     testCase().inFolder             ("./08-arg-binder-validation")
140               .withParams           ("20 20")
141               .expectStatusToBe     (-1)
142               .expectOutputToMatch  ("Expected number to be odd")
143               .finish               (),
144 
145     testCase().inFolder             ("./09-raw-unparsed-arg-list")
146               .withParams           ("echo -- Some args")
147               .expectStatusToBe     (0)
148               .expectOutputToMatch  (`Running command 'echo' with arguments \["Some", "args"\]`)
149               .finish               (),
150     testCase().inFolder             ("./09-raw-unparsed-arg-list")
151               .withParams           ("noarg")
152               .expectStatusToBe     (0)
153               .expectOutputToMatch  (`Running command 'noarg' with arguments \[\]`)
154               .finish               (),
155 
156     testCase().inFolder             ("./10-argument-options")
157               .withParams           ("sensitive --abc 2")
158               .expectStatusToBe     (0)
159               .finish               (),
160     testCase().inFolder             ("./10-argument-options")
161               .withParams           ("sensitive --abC 2")
162               .expectStatusToBe     (-1)
163               .finish               (),
164     testCase().inFolder             ("./10-argument-options")
165               .withParams           ("insensitive --abc 2")
166               .expectStatusToBe     (0)
167               .finish               (),
168     testCase().inFolder             ("./10-argument-options")
169               .withParams           ("insensitive --ABC 2")
170               .expectStatusToBe     (0)
171               .finish               (),
172     testCase().inFolder             ("./10-argument-options")
173               .withParams           ("redefine --abc 2")
174               .expectStatusToBe     (2)
175               .finish               (),
176     testCase().inFolder             ("./10-argument-options")
177               .withParams           ("redefine --abc 2 --abc 1")
178               .expectStatusToBe     (1)
179               .finish               (),
180     testCase().inFolder             ("./10-argument-options")
181               .withParams           ("no-redefine --abc 2")
182               .expectStatusToBe     (0)
183               .finish               (),
184     testCase().inFolder             ("./10-argument-options")
185               .withParams           ("no-redefine --abc 2 --abc 1")
186               .expectStatusToBe     (-1)
187               .finish               (),
188 ];
189  
190 /++ MAIN ++/
191 int main(string[] args)
192 {
193     return (new CommandLineInterface!test()).parseAndExecute(args);
194 }
195 
196 /++ COMMANDS ++/
197 @CommandDefault("Runs all test cases")
198 struct DefaultCommand
199 {
200     int onExecute()
201     {
202         writefln("Running %s tests.", "ALL".ansi.fg(Ansi4BitColour.green));
203         const anyfails = runTestSet(TEST_CASES);
204 
205         return anyfails ? -1 : 0;
206     }
207 }
208 
209 @Command("cleanup", "Runs the cleanup command for all test cases")
210 struct CleanupCommand
211 {
212     void onExecute()
213     {
214         foreach(test; TEST_CASES)
215             runCleanup(test);
216     }
217 }
218 
219 /++ FUNCS ++/
220 bool runTestSet(TestCase[] testSet)
221 {
222     auto results = new TestResult[testSet.length];
223     foreach(i, testCase; testSet)
224         results[i] = testCase.runTest();
225 
226     if(results.any!(r => !r.passed))
227         writefln("\n\nThe following tests %s:", "FAILED".ansi.fg(Ansi4BitColour.red));
228 
229     size_t failedCount;
230     foreach(failed; results.filter!(r => !r.passed))
231     {
232         failedCount++;
233         writefln("\t%s", failed.testCase.to!string.ansi.fg(Ansi4BitColour.cyan));
234 
235         foreach(reason; failed.failedReasons)
236             writefln("\t\t- %s".ansi.fg(Ansi4BitColour.red).to!string, reason);
237     }
238 
239     size_t passedCount = results.length - failedCount;
240     writefln(
241         "\n%s %s, %s %s, %s total tests",
242         passedCount, "PASSED".ansi.fg(Ansi4BitColour.green),
243         failedCount, "FAILED".ansi.fg(Ansi4BitColour.red),
244         results.length
245     );
246 
247     return (failedCount != 0);
248 }
249 
250 TestResult runTest(TestCase testCase)
251 {
252     const    results = getBuildAndTestResults(testCase);
253     auto     passed  = true;
254     string[] reasons;
255 
256     void failIf(bool condition, string reason)
257     {
258         if(!condition)
259             return;
260 
261         passed   = false;
262         reasons ~= reason;
263     }
264 
265     failIf(results[0].status != 0, "Build failed.");
266 
267     // When handling the status code, some terminals allow negative status codes, some don't, so we'll special case expecting
268     // a -1 as expecting -1 or 255.
269     if(testCase.expectedStatus.get(0) != -1)
270         failIf(results[1].status != testCase.expectedStatus.get(0), "Status code is wrong.");
271     else
272         failIf(results[1].status != -1 && results[1].status != 255, "Status code is wrong. (-1 special case)");
273 
274     if(!testCase.outputRegex.isNull)
275         failIf(!results[1].output.matchFirst(testCase.outputRegex.get), "Output doesn't contain a match for the given regex.");
276 
277     if(testCase.allowedToFail)
278     {
279         if(!passed)
280             writeln("Test FAILED (ALLOWED).".ansi.fg(Ansi4BitColour.yellow));
281         passed = true;
282     }
283 
284     if(!passed)
285         writeln("Test FAILED".ansi.fg(Ansi4BitColour.red));
286     else
287         writefln("%s", "Test PASSED".ansi.fg(Ansi4BitColour.green));
288 
289     return TestResult(passed, reasons, testCase);
290 }
291 
292 // [0] = build result, [1] = test result
293 auto getBuildAndTestResults(TestCase testCase)
294 {
295     const CATEGORY_COLOUR = Ansi4BitColour.magenta;
296     const VALUE_COLOUR    = Ansi4BitColour.brightBlue;
297     const RESULT_COLOUR   = Ansi4BitColour.yellow;
298 
299     writefln("");
300     writefln("%s", "[Test Case]".ansi.fg(CATEGORY_COLOUR));
301     writefln("%s: %s", "Folder".ansi.fg(CATEGORY_COLOUR),  testCase.folder.ansi.fg(VALUE_COLOUR));
302     writefln("%s: %s", "Params".ansi.fg(CATEGORY_COLOUR),  testCase.params.ansi.fg(VALUE_COLOUR));
303     writefln("%s: %s", "Status".ansi.fg(CATEGORY_COLOUR),  testCase.expectedStatus.get(0).to!string.ansi.fg(RESULT_COLOUR));
304     writefln("%s: %s", "Regex ".ansi.fg(CATEGORY_COLOUR),  testCase.outputRegex.get(regex("N/A")).to!string.ansi.fg(RESULT_COLOUR));
305 
306     auto cwd = getcwd();
307     chdir(testCase.folder);
308     scope(exit) chdir(cwd);
309 
310     foreach(file; testCase.cleanupFiles.filter!(f => f.exists))
311     {
312         writefln("Cleanup: %s".ansi.fg(Ansi4BitColour.brightBlack).to!string, file);
313         remove(file);
314     }
315 
316     const buildString   = "dub build --compiler=ldc2";
317     const commandString = "\"./test\" " ~ testCase.params;
318     const buildResult   = executeShell(buildString);
319     const result        = executeShell(commandString);
320 
321     writefln("\n%s(status: %s):\n%s", "Build output".ansi.fg(CATEGORY_COLOUR), buildResult.status.to!string.ansi.fg(RESULT_COLOUR), buildResult.output);
322     writefln("%s(status: %s):\n%s",   "Test output".ansi.fg(CATEGORY_COLOUR),  result.status.to!string.ansi.fg(RESULT_COLOUR),      result.output);
323 
324     return [buildResult, result];
325 }
326 
327 void runCleanup(TestCase testCase)
328 {
329     auto cwd = getcwd();
330     chdir(testCase.folder);
331     scope(exit) chdir(cwd);
332     executeShell("dub clean");
333 }