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 ("./08-arg-binder-validation") 126 .withParams ("20 69") 127 .expectStatusToBe (0) 128 .finish (), 129 testCase().inFolder ("./08-arg-binder-validation") 130 .withParams ("69 69") 131 .expectStatusToBe (-1) 132 .expectOutputToMatch ("Expected number to be even") 133 .finish (), 134 testCase().inFolder ("./08-arg-binder-validation") 135 .withParams ("20 20") 136 .expectStatusToBe (-1) 137 .expectOutputToMatch ("Expected number to be odd") 138 .finish (), 139 140 testCase().inFolder ("./09-raw-unparsed-arg-list") 141 .withParams ("echo -- Some args") 142 .expectStatusToBe (0) 143 .expectOutputToMatch (`Running command 'echo' with arguments \["Some", "args"\]`) 144 .finish (), 145 testCase().inFolder ("./09-raw-unparsed-arg-list") 146 .withParams ("noarg") 147 .expectStatusToBe (0) 148 .expectOutputToMatch (`Running command 'noarg' with arguments \[\]`) 149 .finish (), 150 151 testCase().inFolder ("./10-argument-options") 152 .withParams ("sensitive --abc 2") 153 .expectStatusToBe (0) 154 .finish (), 155 testCase().inFolder ("./10-argument-options") 156 .withParams ("sensitive --abC 2") 157 .expectStatusToBe (-1) 158 .finish (), 159 testCase().inFolder ("./10-argument-options") 160 .withParams ("insensitive --abc 2") 161 .expectStatusToBe (0) 162 .finish (), 163 testCase().inFolder ("./10-argument-options") 164 .withParams ("insensitive --ABC 2") 165 .expectStatusToBe (0) 166 .finish (), 167 testCase().inFolder ("./10-argument-options") 168 .withParams ("redefine --abc 2") 169 .expectStatusToBe (2) 170 .finish (), 171 testCase().inFolder ("./10-argument-options") 172 .withParams ("redefine --abc 2 --abc 1") 173 .expectStatusToBe (1) 174 .finish (), 175 testCase().inFolder ("./10-argument-options") 176 .withParams ("no-redefine --abc 2") 177 .expectStatusToBe (0) 178 .finish (), 179 testCase().inFolder ("./10-argument-options") 180 .withParams ("no-redefine --abc 2 --abc 1") 181 .expectStatusToBe (-1) 182 .finish (), 183 ]; 184 185 /++ MAIN ++/ 186 int main(string[] args) 187 { 188 return (new CommandLineInterface!test()).parseAndExecute(args); 189 } 190 191 /++ COMMANDS ++/ 192 @CommandDefault("Runs all test cases") 193 struct DefaultCommand 194 { 195 int onExecute() 196 { 197 writefln("Running %s tests.", "ALL".ansi.fg(Ansi4BitColour.green)); 198 const anyfails = runTestSet(TEST_CASES); 199 200 return anyfails ? -1 : 0; 201 } 202 } 203 204 @Command("cleanup", "Runs the cleanup command for all test cases") 205 struct CleanupCommand 206 { 207 void onExecute() 208 { 209 foreach(test; TEST_CASES) 210 runCleanup(test); 211 } 212 } 213 214 /++ FUNCS ++/ 215 bool runTestSet(TestCase[] testSet) 216 { 217 auto results = new TestResult[testSet.length]; 218 foreach(i, testCase; testSet) 219 results[i] = testCase.runTest(); 220 221 if(results.any!(r => !r.passed)) 222 writefln("\n\nThe following tests %s:", "FAILED".ansi.fg(Ansi4BitColour.red)); 223 224 size_t failedCount; 225 foreach(failed; results.filter!(r => !r.passed)) 226 { 227 failedCount++; 228 writefln("\t%s", failed.testCase.to!string.ansi.fg(Ansi4BitColour.cyan)); 229 230 foreach(reason; failed.failedReasons) 231 writefln("\t\t- %s".ansi.fg(Ansi4BitColour.red).to!string, reason); 232 } 233 234 size_t passedCount = results.length - failedCount; 235 writefln( 236 "\n%s %s, %s %s, %s total tests", 237 passedCount, "PASSED".ansi.fg(Ansi4BitColour.green), 238 failedCount, "FAILED".ansi.fg(Ansi4BitColour.red), 239 results.length 240 ); 241 242 return (failedCount != 0); 243 } 244 245 TestResult runTest(TestCase testCase) 246 { 247 const results = getBuildAndTestResults(testCase); 248 auto passed = true; 249 string[] reasons; 250 251 void failIf(bool condition, string reason) 252 { 253 if(!condition) 254 return; 255 256 passed = false; 257 reasons ~= reason; 258 } 259 260 failIf(results[0].status != 0, "Build failed."); 261 262 // When handling the status code, some terminals allow negative status codes, some don't, so we'll special case expecting 263 // a -1 as expecting -1 or 255. 264 if(testCase.expectedStatus.get(0) != -1) 265 failIf(results[1].status != testCase.expectedStatus.get(0), "Status code is wrong."); 266 else 267 failIf(results[1].status != -1 && results[1].status != 255, "Status code is wrong. (-1 special case)"); 268 269 if(!testCase.outputRegex.isNull) 270 failIf(!results[1].output.matchFirst(testCase.outputRegex.get), "Output doesn't contain a match for the given regex."); 271 272 if(testCase.allowedToFail) 273 { 274 if(!passed) 275 writeln("Test FAILED (ALLOWED).".ansi.fg(Ansi4BitColour.yellow)); 276 passed = true; 277 } 278 279 if(!passed) 280 writeln("Test FAILED".ansi.fg(Ansi4BitColour.red)); 281 else 282 writefln("%s", "Test PASSED".ansi.fg(Ansi4BitColour.green)); 283 284 return TestResult(passed, reasons, testCase); 285 } 286 287 // [0] = build result, [1] = test result 288 auto getBuildAndTestResults(TestCase testCase) 289 { 290 const CATEGORY_COLOUR = Ansi4BitColour.magenta; 291 const VALUE_COLOUR = Ansi4BitColour.brightBlue; 292 const RESULT_COLOUR = Ansi4BitColour.yellow; 293 294 writefln(""); 295 writefln("%s", "[Test Case]".ansi.fg(CATEGORY_COLOUR)); 296 writefln("%s: %s", "Folder".ansi.fg(CATEGORY_COLOUR), testCase.folder.ansi.fg(VALUE_COLOUR)); 297 writefln("%s: %s", "Params".ansi.fg(CATEGORY_COLOUR), testCase.params.ansi.fg(VALUE_COLOUR)); 298 writefln("%s: %s", "Status".ansi.fg(CATEGORY_COLOUR), testCase.expectedStatus.get(0).to!string.ansi.fg(RESULT_COLOUR)); 299 writefln("%s: %s", "Regex ".ansi.fg(CATEGORY_COLOUR), testCase.outputRegex.get(regex("N/A")).to!string.ansi.fg(RESULT_COLOUR)); 300 301 auto cwd = getcwd(); 302 chdir(testCase.folder); 303 scope(exit) chdir(cwd); 304 305 foreach(file; testCase.cleanupFiles.filter!(f => f.exists)) 306 { 307 writefln("Cleanup: %s".ansi.fg(Ansi4BitColour.brightBlack).to!string, file); 308 remove(file); 309 } 310 311 const buildString = "dub build --compiler=ldc2"; 312 const commandString = "\"./test\" " ~ testCase.params; 313 const buildResult = executeShell(buildString); 314 const result = executeShell(commandString); 315 316 writefln("\n%s(status: %s):\n%s", "Build output".ansi.fg(CATEGORY_COLOUR), buildResult.status.to!string.ansi.fg(RESULT_COLOUR), buildResult.output); 317 writefln("%s(status: %s):\n%s", "Test output".ansi.fg(CATEGORY_COLOUR), result.status.to!string.ansi.fg(RESULT_COLOUR), result.output); 318 319 return [buildResult, result]; 320 } 321 322 void runCleanup(TestCase testCase) 323 { 324 auto cwd = getcwd(); 325 chdir(testCase.folder); 326 scope(exit) chdir(cwd); 327 executeShell("dub clean"); 328 }