1 /+dub.sdl: 2 name "test" 3 dependency "jcli" path="../" 4 +/ 5 module test; 6 7 import std, jaster.cli; 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 } 18 19 struct TestCaseBuilder 20 { 21 TestCase testCase; 22 23 TestCaseBuilder inFolder(string folder) 24 { 25 this.testCase.folder = folder; 26 return this; 27 } 28 29 TestCaseBuilder withParams(string params) 30 { 31 this.testCase.params = params; 32 return this; 33 } 34 35 TestCaseBuilder expectStatusToBe(int status) 36 { 37 this.testCase.expectedStatus = status; 38 return this; 39 } 40 41 TestCaseBuilder expectOutputToMatch(string regexString) 42 { 43 this.testCase.outputRegex = regex(regexString); 44 return this; 45 } 46 47 TestCaseBuilder cleanup(string fileName) 48 { 49 this.testCase.cleanupFiles ~= fileName; 50 return this; 51 } 52 53 TestCase finish() 54 { 55 return this.testCase; 56 } 57 } 58 TestCaseBuilder testCase(){ return TestCaseBuilder(); } 59 60 struct TestResult 61 { 62 bool passed; 63 string[] failedReasons; 64 TestCase testCase; 65 } 66 67 /++ CONFIGURATION ++/ 68 auto TEST_CASES = 69 [ 70 testCase().inFolder ("./00-basic-usage-default-command/") 71 .withParams ("20") 72 .expectStatusToBe (0) 73 .finish (), 74 testCase().inFolder ("./00-basic-usage-default-command/") 75 .withParams ("20 --reverse") 76 .expectStatusToBe (128) 77 .finish (), 78 79 testCase().inFolder ("./01-named-sub-commands/") 80 .withParams ("return 0") 81 .expectStatusToBe (0) 82 .finish (), 83 testCase().inFolder ("./01-named-sub-commands/") 84 .withParams ("r 128") 85 .expectStatusToBe (128) 86 .finish (), 87 88 testCase().inFolder ("./02-shorthand-longhand-args/") 89 .withParams ("return --code 0") 90 .expectStatusToBe (0) 91 .finish (), 92 testCase().inFolder ("./02-shorthand-longhand-args/") 93 .withParams ("r -c=128") 94 .expectStatusToBe (128) 95 .finish (), 96 97 testCase().inFolder ("./03-inheritence-base-commands/") 98 .withParams ("add 1 2") 99 .expectStatusToBe (3) 100 .finish (), 101 testCase().inFolder ("./03-inheritence-base-commands/") 102 .withParams ("add 1 2 --offset=7") 103 .expectStatusToBe (10) 104 .finish (), 105 106 testCase().inFolder ("./04-custom-arg-binders/") 107 .withParams ("./dub.sdl") 108 .expectStatusToBe (0) 109 .finish (), 110 testCase().inFolder ("./04-custom-arg-binders/") 111 .withParams ("./lalaland.txt") 112 .expectStatusToBe (-1) 113 .finish (), 114 115 testCase().inFolder ("./05-dependency-injection/") 116 .withParams ("dman") 117 .expectStatusToBe (0) 118 .finish (), 119 testCase().inFolder ("./05-dependency-injection/") 120 .withParams ("cman") 121 .expectStatusToBe (128) 122 .finish (), 123 124 testCase().inFolder ("./06-configuration") 125 .withParams ("force exception") 126 .expectOutputToMatch ("$^") // Match nothing 127 .cleanup ("config.json") // Otherwise subsequent runs of this test set won't work. 128 .finish (), 129 testCase().inFolder ("./06-configuration") 130 .withParams ("set verbose true") 131 .expectOutputToMatch ("$^") 132 .finish (), 133 testCase().inFolder ("./06-configuration") 134 .withParams ("set name Bradley") 135 .expectOutputToMatch (".*") // Verbose logging should kick in 136 .finish (), 137 testCase().inFolder ("./06-configuration") 138 .withParams ("force exception") 139 .expectOutputToMatch (".*") // Ditto 140 .finish (), 141 testCase().inFolder ("./06-configuration") 142 .withParams ("greet") 143 .expectOutputToMatch ("Brad") 144 .finish (), 145 146 // Can't use expectOutputToMatch for non-coloured text as it doesn't handle ANSI properly. 147 testCase().inFolder ("./07-text-buffer-table") 148 .withParams ("fixed") 149 .expectStatusToBe (0) 150 .expectOutputToMatch ("Age") 151 .finish (), 152 testCase().inFolder ("./07-text-buffer-table") 153 .withParams ("dynamic") 154 .expectStatusToBe (0) 155 .expectOutputToMatch ("Age") 156 .finish (), 157 158 testCase().inFolder ("./08-arg-binder-validation") 159 .withParams ("20 69") 160 .expectStatusToBe (0) 161 .finish (), 162 testCase().inFolder ("./08-arg-binder-validation") 163 .withParams ("69 69") 164 .expectStatusToBe (-1) 165 .expectOutputToMatch ("Expected number to be even") 166 .finish (), 167 testCase().inFolder ("./08-arg-binder-validation") 168 .withParams ("20 20") 169 .expectStatusToBe (-1) 170 .expectOutputToMatch ("Expected number to be odd") 171 .finish (), 172 173 testCase().inFolder ("./09-raw-unparsed-arg-list") 174 .withParams ("echo -- Some args") 175 .expectStatusToBe (0) 176 .expectOutputToMatch (`Running command 'echo' with arguments \["Some", "args"\]`) 177 .finish (), 178 testCase().inFolder ("./09-raw-unparsed-arg-list") 179 .withParams ("noarg") 180 .expectStatusToBe (0) 181 .expectOutputToMatch (`Running command 'noarg' with arguments \[\]`) 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 UserIO.logInfof("Running %s tests.", "ALL".ansi.fg(Ansi4BitColour.green)); 198 const anyFailures = runTestSet(TEST_CASES); 199 200 return anyFailures ? -1 : 0; 201 } 202 } 203 204 /++ FUNCS ++/ 205 bool runTestSet(TestCase[] testSet) 206 { 207 auto results = new TestResult[testSet.length]; 208 foreach(i, testCase; testSet) 209 results[i] = testCase.runTest(); 210 211 if(results.any!(r => !r.passed)) 212 UserIO.logInfof("\n\nThe following tests %s:", "FAILED".ansi.fg(Ansi4BitColour.red)); 213 214 size_t failedCount; 215 foreach(failed; results.filter!(r => !r.passed)) 216 { 217 failedCount++; 218 UserIO.logInfof("\t%s", failed.testCase.to!string.ansi.fg(Ansi4BitColour.cyan)); 219 220 foreach(reason; failed.failedReasons) 221 UserIO.logErrorf("\t\t- %s", reason); 222 } 223 224 size_t passedCount = results.length - failedCount; 225 UserIO.logInfof( 226 "\n%s %s, %s %s, %s total tests", 227 passedCount, "PASSED".ansi.fg(Ansi4BitColour.green), 228 failedCount, "FAILED".ansi.fg(Ansi4BitColour.red), 229 results.length 230 ); 231 232 return (failedCount != 0); 233 } 234 235 TestResult runTest(TestCase testCase) 236 { 237 const results = getBuildAndTestResults(testCase); 238 auto passed = true; 239 string[] reasons; 240 241 void failIf(bool condition, string reason) 242 { 243 if(!condition) 244 return; 245 246 passed = false; 247 reasons ~= reason; 248 } 249 250 failIf(results[0].statusCode != 0, "Build failed."); 251 252 // When handling the status code, some terminals allow negative status codes, some don't, so we'll special case expecting 253 // a -1 as expecting -1 or 255. 254 if(testCase.expectedStatus.get(0) != -1) 255 failIf(results[1].statusCode != testCase.expectedStatus.get(0), "Status code is wrong."); 256 else 257 failIf(results[1].statusCode != -1 && results[1].statusCode != 255, "Status code is wrong. (-1 special case)"); 258 259 if(!testCase.outputRegex.isNull) 260 failIf(!results[1].output.match(testCase.outputRegex), "Output doesn't contain a match for the given regex."); 261 262 if(!passed) 263 UserIO.logErrorf("Test FAILED"); 264 else 265 UserIO.logInfof("%s", "Test PASSED".ansi.fg(Ansi4BitColour.green)); 266 267 return TestResult(passed, reasons, testCase); 268 } 269 270 // [0] = build result, [1] = test result 271 Shell.Result[2] getBuildAndTestResults(TestCase testCase) 272 { 273 const CATEGORY_COLOUR = Ansi4BitColour.magenta; 274 const VALUE_COLOUR = Ansi4BitColour.brightBlue; 275 const RESULT_COLOUR = Ansi4BitColour.yellow; 276 277 UserIO.logInfof(""); 278 UserIO.logInfof("%s", "[Test Case]".ansi.fg(CATEGORY_COLOUR)); 279 UserIO.logInfof("%s: %s", "Folder".ansi.fg(CATEGORY_COLOUR), testCase.folder.ansi.fg(VALUE_COLOUR)); 280 UserIO.logInfof("%s: %s", "Params".ansi.fg(CATEGORY_COLOUR), testCase.params.ansi.fg(VALUE_COLOUR)); 281 UserIO.logInfof("%s: %s", "Status".ansi.fg(CATEGORY_COLOUR), testCase.expectedStatus.get(0).to!string.ansi.fg(RESULT_COLOUR)); 282 UserIO.logInfof("%s: %s", "Regex ".ansi.fg(CATEGORY_COLOUR), testCase.outputRegex.get(regex("N/A")).to!string.ansi.fg(RESULT_COLOUR)); 283 284 Shell.pushLocation(testCase.folder); 285 scope(exit) Shell.popLocation(); 286 287 foreach(file; testCase.cleanupFiles.filter!(f => f.exists)) 288 { 289 UserIO.logTracef("Cleanup: %s", file); 290 remove(file); 291 } 292 293 const buildString = "dub build --compiler=ldc2"; 294 const commandString = "\"./test\" " ~ testCase.params; 295 const buildResult = Shell.execute(buildString); 296 const result = Shell.execute(commandString); 297 298 UserIO.logInfof("\n%s(status: %s):\n%s", "Build output".ansi.fg(CATEGORY_COLOUR), buildResult.statusCode.to!string.ansi.fg(RESULT_COLOUR), buildResult.output); 299 UserIO.logInfof("%s(status: %s):\n%s", "Test output".ansi.fg(CATEGORY_COLOUR), result.statusCode.to!string.ansi.fg(RESULT_COLOUR), result.output); 300 301 return [buildResult, result]; 302 }