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 int buildStatus; 74 int runStatus; 75 string buildOut; 76 string runOut; 77 } 78 79 /++ CONFIGURATION ++/ 80 auto TEST_CASES = 81 [ 82 testCase().inFolder ("./00-basic-usage-default-command/") 83 .withParams ("20") 84 .expectStatusToBe (0) 85 .finish (), 86 testCase().inFolder ("./00-basic-usage-default-command/") 87 .withParams ("20 --reverse") 88 .expectStatusToBe (128) 89 .finish (), 90 91 testCase().inFolder ("./01-named-sub-commands/") 92 .withParams ("return 0") 93 .expectStatusToBe (0) 94 .finish (), 95 testCase().inFolder ("./01-named-sub-commands/") 96 .withParams ("r 128") 97 .expectStatusToBe (128) 98 .finish (), 99 100 testCase().inFolder ("./02-shorthand-longhand-args/") 101 .withParams ("return --code 0") 102 .expectStatusToBe (0) 103 .finish (), 104 testCase().inFolder ("./02-shorthand-longhand-args/") 105 .withParams ("r -c=128") 106 .expectStatusToBe (128) 107 .finish (), 108 109 // Class inheritence is broken, but I don't really think I can fix it without compiler changes. 110 testCase().inFolder ("./03-inheritence-base-commands/") 111 .withParams ("add 1 2") 112 .expectStatusToBe (3) 113 .allowToFail () 114 .finish (), 115 testCase().inFolder ("./03-inheritence-base-commands/") 116 .withParams ("add 1 2 --offset=7") 117 .expectStatusToBe (10) 118 .allowToFail () 119 .finish (), 120 121 testCase().inFolder ("./04-custom-arg-binders/") 122 .withParams ("./dub.sdl") 123 .expectStatusToBe (0) 124 .finish (), 125 testCase().inFolder ("./04-custom-arg-binders/") 126 .withParams ("./lalaland.txt") 127 .expectStatusToBe (-1) 128 .finish (), 129 130 testCase().inFolder ("./05-built-in-binders/") 131 .withParams ("echo -b -i 2 -f 2.2 -s Hola -e red") 132 .expectStatusToBe (0) 133 .finish (), 134 135 testCase().inFolder ("./06-result-with-fail-code/") 136 .withParams ("abc") 137 // .expectStatusToBe (100) 138 .expectStatusToBe (-1) 139 .finish (), 140 testCase().inFolder ("./06-result-with-fail-code/") 141 .withParams ("0") 142 // .expectStatusToBe (102) 143 .expectStatusToBe (-1) 144 .finish (), 145 testCase().inFolder ("./06-result-with-fail-code/") 146 .withParams ("3") 147 // .expectStatusToBe (101) 148 .expectStatusToBe (-1) 149 .finish (), 150 testCase().inFolder ("./06-result-with-fail-code/") 151 .withParams ("1") 152 .expectStatusToBe (0) 153 .finish (), 154 155 testCase().inFolder ("./08-arg-binder-validation") 156 .withParams ("20 69") 157 .expectStatusToBe (0) 158 .finish (), 159 testCase().inFolder ("./08-arg-binder-validation") 160 .withParams ("69 69") 161 .expectStatusToBe (-1) 162 .expectOutputToMatch ("Expected number to be even") 163 .finish (), 164 testCase().inFolder ("./08-arg-binder-validation") 165 .withParams ("20 20") 166 .expectStatusToBe (-1) 167 .expectOutputToMatch ("Expected number to be odd") 168 .finish (), 169 170 testCase().inFolder ("./09-raw-unparsed-arg-list") 171 .withParams ("echo -- Some args") 172 .expectStatusToBe (0) 173 .expectOutputToMatch (`Running command 'echo' with arguments \["Some", "args"\]`) 174 .finish (), 175 testCase().inFolder ("./09-raw-unparsed-arg-list") 176 .withParams ("noarg") 177 .expectStatusToBe (0) 178 .expectOutputToMatch (`Running command 'noarg' with arguments \[\]`) 179 .finish (), 180 181 testCase().inFolder ("./10-argument-options") 182 .withParams ("sensitive --abc 2") 183 .expectStatusToBe (0) 184 .finish (), 185 testCase().inFolder ("./10-argument-options") 186 .withParams ("sensitive --abC 2") 187 .expectStatusToBe (-1) 188 .finish (), 189 testCase().inFolder ("./10-argument-options") 190 .withParams ("insensitive --abc 2") 191 .expectStatusToBe (0) 192 .finish (), 193 testCase().inFolder ("./10-argument-options") 194 .withParams ("insensitive --ABC 2") 195 .expectStatusToBe (0) 196 .finish (), 197 testCase().inFolder ("./10-argument-options") 198 .withParams ("redefine --abc 2") 199 .expectStatusToBe (2) 200 .finish (), 201 testCase().inFolder ("./10-argument-options") 202 .withParams ("redefine --abc 2 --abc 1") 203 .expectStatusToBe (1) 204 .finish (), 205 testCase().inFolder ("./10-argument-options") 206 .withParams ("no-redefine --abc 2") 207 .expectStatusToBe (0) 208 .finish (), 209 testCase().inFolder ("./10-argument-options") 210 .withParams ("no-redefine --abc 2 --abc 1") 211 .expectStatusToBe (-1) 212 .finish (), 213 ]; 214 215 /++ MAIN ++/ 216 int main(string[] args) 217 { 218 return (new CommandLineInterface!test()).parseAndExecute(args); 219 } 220 221 /++ COMMANDS ++/ 222 @CommandDefault("Runs all test cases") 223 struct DefaultCommand 224 { 225 int onExecute() 226 { 227 writefln("Running %s tests.", "ALL".ansi.fg(Ansi4BitColour.green)); 228 const anyfails = runTestSet(TEST_CASES); 229 230 return anyfails ? -1 : 0; 231 } 232 } 233 234 @Command("ui", "Runs the test UI") 235 struct UICommand 236 { 237 void onExecute() 238 { 239 auto ui = new UI(); 240 ui.run(); 241 } 242 } 243 244 @Command("cleanup", "Runs the cleanup command for all test cases") 245 struct CleanupCommand 246 { 247 void onExecute() 248 { 249 foreach(test; TEST_CASES) 250 runCleanup(test); 251 } 252 } 253 254 /++ FUNCS ++/ 255 bool runTestSet(TestCase[] testSet) 256 { 257 auto results = new TestResult[testSet.length]; 258 foreach(i, testCase; testSet) 259 results[i] = testCase.runTest(); 260 261 if(results.any!(r => !r.passed)) 262 writefln("\n\nThe following tests %s:", "FAILED".ansi.fg(Ansi4BitColour.red)); 263 264 size_t failedCount; 265 foreach(failed; results.filter!(r => !r.passed)) 266 { 267 failedCount++; 268 writefln("\t%s", failed.testCase.to!string.ansi.fg(Ansi4BitColour.cyan)); 269 270 foreach(reason; failed.failedReasons) 271 writefln("\t\t- %s".ansi.fg(Ansi4BitColour.red).to!string, reason); 272 } 273 274 size_t passedCount = results.length - failedCount; 275 writefln( 276 "\n%s %s, %s %s, %s total tests", 277 passedCount, "PASSED".ansi.fg(Ansi4BitColour.green), 278 failedCount, "FAILED".ansi.fg(Ansi4BitColour.red), 279 results.length 280 ); 281 282 return (failedCount != 0); 283 } 284 285 TestResult runTest(TestCase testCase) 286 { 287 const results = getBuildAndTestResults(testCase); 288 auto passed = true; 289 string[] reasons; 290 291 void failIf(bool condition, string reason) 292 { 293 if(!condition) 294 return; 295 296 passed = false; 297 reasons ~= reason; 298 } 299 300 failIf(results[0].status != 0, "Build failed."); 301 302 // When handling the status code, some terminals allow negative status codes, some don't, so we'll special case expecting 303 // a -1 as expecting -1 or 255. 304 if(testCase.expectedStatus.get(0) != -1) 305 failIf(results[1].status != testCase.expectedStatus.get(0), "Status code is wrong."); 306 else 307 failIf(results[1].status != -1 && results[1].status != 255, "Status code is wrong. (-1 special case)"); 308 309 if(!testCase.outputRegex.isNull) 310 failIf(!results[1].output.matchFirst(testCase.outputRegex.get), "Output doesn't contain a match for the given regex."); 311 312 if(testCase.allowedToFail) 313 { 314 if(!passed) 315 writeln("Test FAILED (ALLOWED).".ansi.fg(Ansi4BitColour.yellow)); 316 passed = true; 317 } 318 319 if(!passed) 320 writeln("Test FAILED".ansi.fg(Ansi4BitColour.red)); 321 else 322 writefln("%s", "Test PASSED".ansi.fg(Ansi4BitColour.green)); 323 324 return TestResult(passed, reasons, testCase, results[0].status, results[1].status, results[0].output, results[1].output); 325 } 326 327 // [0] = build result, [1] = test result 328 auto getBuildAndTestResults(TestCase testCase) 329 { 330 const CATEGORY_COLOUR = Ansi4BitColour.magenta; 331 const VALUE_COLOUR = Ansi4BitColour.brightBlue; 332 const RESULT_COLOUR = Ansi4BitColour.yellow; 333 334 writefln(""); 335 writefln("%s", "[Test Case]".ansi.fg(CATEGORY_COLOUR)); 336 writefln("%s: %s", "Folder".ansi.fg(CATEGORY_COLOUR), testCase.folder.ansi.fg(VALUE_COLOUR)); 337 writefln("%s: %s", "Params".ansi.fg(CATEGORY_COLOUR), testCase.params.ansi.fg(VALUE_COLOUR)); 338 writefln("%s: %s", "Status".ansi.fg(CATEGORY_COLOUR), testCase.expectedStatus.get(0).to!string.ansi.fg(RESULT_COLOUR)); 339 writefln("%s: %s", "Regex ".ansi.fg(CATEGORY_COLOUR), testCase.outputRegex.get(regex("N/A")).to!string.ansi.fg(RESULT_COLOUR)); 340 341 auto cwd = getcwd(); 342 chdir(testCase.folder); 343 scope(exit) chdir(cwd); 344 345 foreach(file; testCase.cleanupFiles.filter!(f => f.exists)) 346 { 347 writefln("Cleanup: %s".ansi.fg(Ansi4BitColour.brightBlack).to!string, file); 348 remove(file); 349 } 350 351 const buildString = "dub build --compiler=ldc2"; 352 const commandString = "\"./test\" " ~ testCase.params; 353 const buildResult = executeShell(buildString); 354 const result = executeShell(commandString); 355 356 writefln("\n%s(status: %s):\n%s", "Build output".ansi.fg(CATEGORY_COLOUR), buildResult.status.to!string.ansi.fg(RESULT_COLOUR), buildResult.output); 357 writefln("%s(status: %s):\n%s", "Test output".ansi.fg(CATEGORY_COLOUR), result.status.to!string.ansi.fg(RESULT_COLOUR), result.output); 358 359 return [buildResult, result]; 360 } 361 362 void runCleanup(TestCase testCase) 363 { 364 auto cwd = getcwd(); 365 chdir(testCase.folder); 366 scope(exit) chdir(cwd); 367 executeShell("dub clean"); 368 } 369 370 final class UI 371 { 372 TextBuffer buffer; 373 Layout layout; 374 size_t selectedTest; 375 size_t buildOffset; 376 size_t buildXOffset; 377 size_t runOffset; 378 379 static struct Test 380 { 381 enum State 382 { 383 notRan, 384 success, 385 failure 386 } 387 string displayName; 388 State state; 389 int buildStatus; 390 int runStatus; 391 string buildOutput; 392 string runOutput; 393 } 394 Test[] tests; 395 396 void run() 397 { 398 Console.attach(); 399 this.buffer = Console.createTextBuffer(); 400 this.layout = Layout( 401 Rect(0, 0, this.buffer.width, this.buffer.height), 402 8, 8 403 ); 404 405 this.tests = TEST_CASES.map!(t => Test(" " ~ t.folder ~ " " ~ t.params ~ " ")).array; 406 407 while(Console.isAttached) 408 { 409 Console.processEvents((e) 410 { 411 e.match!( 412 (ConsoleKeyEvent key) => handleKey(key), 413 (_){} 414 ); 415 }); 416 417 if(Console.isAttached) 418 this.draw(); 419 } 420 } 421 422 private void handleKey(ConsoleKeyEvent key) 423 { 424 if(!key.isDown) 425 return; 426 427 // If I weren't lazy I'd bring the shift and ctrl keys into play, but I'm lazy. 428 if(key.key == ConsoleKey.escape) 429 Console.detach(); 430 else if(key.key == ConsoleKey.up && this.selectedTest != 0) 431 this.selectedTest--; 432 else if(key.key == ConsoleKey.down && this.selectedTest < TEST_CASES.length-1) 433 this.selectedTest++; 434 else if(key.key == ConsoleKey.enter) 435 this.runTest(this.selectedTest); 436 else if(key.key == ConsoleKey.home && this.buildOffset != 0) 437 this.buildOffset--; 438 else if(key.key == ConsoleKey.end) 439 this.buildOffset++; 440 else if(key.key == ConsoleKey.insert && this.runOffset != 0) 441 this.runOffset--; 442 else if(key.key == ConsoleKey.del) 443 this.runOffset++; 444 else if(key.key == ConsoleKey.right) 445 this.buildXOffset++; 446 else if(key.key == ConsoleKey.left && this.buildXOffset != 0) 447 this.buildXOffset--; 448 else if(key.key == ConsoleKey.back) 449 { 450 foreach(i; 0..TEST_CASES.length) 451 this.runTest(i); 452 } 453 } 454 455 private void runTest(size_t test) 456 { 457 const TestResult result = .runTest(TEST_CASES[test]); 458 this.tests[test].state = (result.passed) ? Test.State.success : Test.State.failure; 459 this.tests[test].buildOutput = result.buildOut; 460 this.tests[test].runOutput = result.runOut; 461 } 462 463 private void draw() 464 { 465 auto testBlock = BorderWidgetBuilder() 466 .withBackground(AnsiColour(Ansi4BitColour.black)) 467 .withForeground(AnsiColour(Ansi4BitColour.yellow)) 468 .withBlockArea(Rect(0, 0, 3, 7)) 469 .withBorderStyle(BorderStyle.all) 470 .withTitle("TESTS") 471 .withTitleAlignment(Alignment.center) 472 .build(); 473 474 auto resultBlock = BorderWidgetBuilder() 475 .withBackground(AnsiColour(Ansi4BitColour.black)) 476 .withForeground(AnsiColour(Ansi4BitColour.yellow)) 477 .withBlockArea(Rect(3, 0, 8, 7)) 478 .withBorderStyle(BorderStyle.all) 479 .withTitle("RESULT") 480 .withTitleAlignment(Alignment.center) 481 .build(); 482 483 testBlock.render(this.layout, this.buffer); 484 resultBlock.render(this.layout, this.buffer); 485 486 const testArea = testBlock.innerArea(this.layout); 487 const testLayout = Layout(testArea, testArea.width, testArea.height); 488 foreach(i, test; this.tests[this.selectedTest..$]) 489 { 490 const fg = 491 (test.state == Test.State.notRan) 492 ? AnsiColour.init 493 : (test.state == Test.State.failure) 494 ? AnsiColour(Ansi4BitColour.red) 495 : AnsiColour(Ansi4BitColour.green); 496 497 TextWidgetBuilder() 498 .withBlockArea(Rect(2, cast(int)i, testArea.width, cast(int)i+1)) 499 .withText(test.displayName) 500 .withStyle(i == 0 ? AnsiStyleSet.init.bg(AnsiColour(Ansi4BitColour.blue)).fg(fg) : AnsiStyleSet.init.fg(fg)) 501 .build() 502 .render(testLayout, this.buffer); 503 } 504 505 const resultArea = resultBlock.innerArea(this.layout); 506 const resultLayout = Layout(resultArea, 2, 1); 507 const buildArea = resultLayout.blockRectToRealRect(Rect(0, 0, 1, 1)); 508 const runArea = resultLayout.blockRectToRealRect(Rect(1, 0, 2, 1)); 509 const buildLayout = Layout(buildArea, 1, 1); 510 const runLayout = Layout(runArea, 1, 1); 511 512 auto buildBlock = BorderWidgetBuilder() 513 .withBlockArea(Rect(0, 0, 1, 1)) 514 .withBorderStyle(BorderStyle.all) 515 .withTitle("Build") 516 .withTitleAlignment(Alignment.center) 517 .build(); 518 buildBlock.render(buildLayout, this.buffer); 519 520 auto runBlock = BorderWidgetBuilder() 521 .withBlockArea(Rect(0, 0, 1, 1)) 522 .withBorderStyle(BorderStyle.all) 523 .withTitle("Run") 524 .withTitleAlignment(Alignment.center) 525 .build(); 526 runBlock.render(runLayout, this.buffer); 527 528 const buildOutArea = buildBlock.innerArea(buildLayout); 529 const runOutArea = runBlock.innerArea(runLayout); 530 const buildOutLayout = Layout(buildOutArea, 1, buildOutArea.height); 531 const runOutLayout = Layout(runOutArea, 1, runOutArea.height); 532 533 foreach(i, line; this.tests[this.selectedTest].buildOutput.lineSplitter.drop(this.buildOffset).enumerate) 534 { 535 TextWidgetBuilder() 536 .withBlockArea(Rect(0, cast(int)i, 1, cast(int)i+1)) 537 .withText(line[min(this.buildXOffset, line.length)..$]) 538 .build() 539 .render(buildOutLayout, this.buffer); 540 } 541 542 foreach(i, line; this.tests[this.selectedTest].runOutput.lineSplitter.drop(this.runOffset).enumerate) 543 { 544 TextWidgetBuilder() 545 .withBlockArea(Rect(0, cast(int)i, 1, cast(int)i+1)) 546 .withText(line[min(this.buildXOffset, line.length)..$]) 547 .build() 548 .render(runOutLayout, this.buffer); 549 } 550 551 ShortcutsWidgetBuilder!7() 552 .withBackground(AnsiColour(Ansi4BitColour.brightBlack)) 553 .withKeyStyle(AnsiStyleSet.init.bg(AnsiColour(Ansi4BitColour.blue))) 554 .withDescriptionStyle(AnsiStyleSet.init.bg(AnsiColour(Ansi4BitColour.brightBlack))) 555 .withShortcut(0, "ESC", "Close UI") 556 .withShortcut(1, "↑↓", "Select test") 557 .withShortcut(2, "ENTER", "Run test") 558 .withShortcut(3, "BACKSPACE", "Run all tests") 559 .withShortcut(4, "INS DEL", "Move Build Output") 560 .withShortcut(5, "PGUP PGDN", "Move Run Output") 561 .withShortcut(6, "←→", "Offset Output") 562 .build() 563 .render(this.buffer); 564 565 this.buffer.refresh(); 566 } 567 }