1 module feature_test.runner; 2 3 debug (featureTest) { 4 import feature_test.core; 5 import feature_test.exceptions; 6 import feature_test.callbacks; 7 8 import colorize; 9 10 import std.string; 11 import std.stdio; 12 import std.conv; 13 import std.random; 14 import std.algorithm; 15 import std.array; 16 17 import core.exception; 18 19 class FeatureTestRunner { 20 mixin FTCallbacks; 21 22 struct Failure { 23 string feature; 24 string scenario; 25 Throwable detail; 26 } 27 28 uint featuresTested; 29 uint scenariosPassed; 30 31 FeatureTest[] features; 32 Failure[] failures; 33 Failure[] pending; 34 string[] onlyTags; 35 string[] ignoreTags; 36 37 this() { 38 randomize = true; 39 quiet = false; 40 _displayWidth = 80; 41 } 42 43 static @property FeatureTestRunner instance() { 44 if (!_runner) _runner = new FeatureTestRunner; 45 return _runner; 46 } 47 48 static this() { 49 import core.runtime; 50 51 writeln("Feature Testing Enabled!".color(fg.light_green)); 52 foreach(arg; Runtime.args) { 53 if (arg[0] == '@') { instance.addOnlyTags(arg[1..$].split(",")); } 54 if (arg[0] == '+') { instance.removeIgnoreTags(arg[1..$].split(",")); } 55 else if (arg[0] == '-') { instance.addIgnoreTags(arg[1..$].split(",")); } 56 else if (arg == "quiet") instance.quiet = true; 57 } 58 } 59 60 static ~this() { 61 if (instance.onlyTags.length) instance.logln("Only including tags: ".color(fg.light_yellow) ~ format(instance.onlyTags.join(", ").color(fg.light_white))); 62 if (instance.ignoreTags.length) instance.logln("Ignoring tags: ".color(fg.light_magenta) ~ format(instance.ignoreTags.join(", ").color(fg.light_white))); 63 if (instance.randomize) { 64 instance.logln("Randomizing Features".color(fg.light_cyan)); 65 instance.features.randomShuffle; 66 } 67 68 instance.runBeforeAll; 69 foreach(feature; _runner.features) { 70 instance.runBeforeEach; 71 _runner.runFeature(feature); 72 instance.runAfterEach; 73 } 74 instance.runAfterAll; 75 writeln(_runner.report); 76 } 77 78 /// Adds the given tag to onlyTags if it doesn't already exist 79 void addOnlyTag(string tag) { 80 if (!onlyTags.canFind(tag)) onlyTags ~= tag; 81 } 82 83 /// Adds each of the given tags to onlyTags if it doesn't already exist 84 void addOnlyTags(string[] tags ...) { 85 foreach(tag; tags) addOnlyTag(tag); 86 } 87 88 /// Adds the given tag to ignoreTags if it doesn't already exist 89 void addIgnoreTag(string tag) { 90 if (!ignoreTags.canFind(tag)) ignoreTags ~= tag; 91 } 92 93 /// Adds each of the given tags to ignoreTags if it doesn't already exist 94 void addIgnoreTags(string[] tags ...) { 95 foreach(tag; tags) addIgnoreTag(tag); 96 } 97 98 99 /// Removes the given tag to ignoreTags if it exists 100 void removeIgnoreTag(string tag) { 101 ignoreTags = array(ignoreTags.filter!(a => a != tag)); 102 } 103 104 /// Removes each of the given tags from ignoreTags if they exist 105 void removeIgnoreTags(string[] tags ...) { 106 foreach(tag; tags) removeIgnoreTag(tag); 107 } 108 109 /// Returns true if a feature with the given tags should be included 110 bool shouldInclude(string[] tags ...) { 111 foreach(tag; ignoreTags) if (tags.canFind(tag)) return false; 112 if (!onlyTags.length) return true; 113 foreach(tag; onlyTags) if (tags.canFind(tag)) return true; 114 return false; 115 } 116 117 @property scenariosTested() { 118 return scenariosPassed + failures.length; 119 } 120 121 void incFeatures() { featuresTested += 1; } 122 void incPassed() { scenariosPassed += 1; } 123 124 void reset() { 125 featuresTested = 0; 126 scenariosPassed = 0; 127 failures = []; 128 pending = []; 129 } 130 131 string report() { 132 string output; 133 134 if (pending.length) { 135 output ~= "\n!!! Pending !!!\n\n".color(fg.light_yellow); 136 foreach(failure; pending) { 137 output ~= format("%s %s\n", "Feature:".color(fg.light_yellow), failure.feature.color(fg.light_white, bg.init, mode.bold)); 138 output ~= format("\t%s\n".color(fg.light_yellow), failure.scenario); 139 output ~= format("\t%s(%s)\n".color(fg.cyan), failure.detail.file, failure.detail.line); 140 output ~= format("\t%s\n\n", failure.detail.msg); 141 } 142 } 143 144 if (failures.length) { 145 output ~= "\n!!! Failures !!!\n\n".color(fg.light_red); 146 foreach(failure; failures) { 147 output ~= format("%s %s\n", "Feature:".color(fg.light_yellow), failure.feature.color(fg.light_white, bg.init, mode.bold)); 148 output ~= format("\t%s\n".color(fg.light_red), failure.scenario); 149 output ~= format("\t%s(%s)\n".color(fg.cyan), failure.detail.file, failure.detail.line); 150 output ~= format("\t%s\n\n", failure.detail.msg); 151 } 152 } 153 else { 154 output ~= "All feature tests passed successfully!\n".color(fg.light_green); 155 } 156 157 output ~= format(" Features tested: %s\n", featuresTested.to!string.color(fg.light_cyan)); 158 output ~= format(" Scenarios tested: %s\n", scenariosTested.to!string.color(fg.light_cyan)); 159 if (scenariosPassed) output ~= format(" Scenarios passed: %s\n", scenariosPassed.to!string.color(fg.light_green)); 160 if (failures.length) output ~= format(" Scenarios failed: %s\n", failures.length.to!string.color(fg.light_red)); 161 if (pending.length) output ~= format("Scenarios pending: %s\n", pending.length.to!string.color(fg.light_yellow)); 162 163 return output; 164 } 165 166 167 // Functions for indenting output appropriately; 168 169 @property ref uint indent() { 170 return _indent; 171 } 172 173 void log(T)(T output) { 174 auto indentString = indentTabs; 175 output = output.wrap(_displayWidth, indentString, indentString, indentString.length); 176 write(output.stripRight); 177 } 178 179 void logln(T)(T output) { 180 auto indentString = indentTabs; 181 output = output.wrap(_displayWidth, indentString, indentString, indentString.length); 182 write(output); 183 } 184 185 void logf(T, A ...)(T fmt, A args) { 186 auto output = format(fmt, args); 187 log(output); 188 } 189 190 void logfln(T, A ...)(T fmt, A args) { 191 logf(fmt, args); 192 writeln(); 193 } 194 195 void info(A ...)(string fmt, A args) { 196 logfln(fmt.color(fg.light_blue), args); 197 } 198 199 void runFeature(FeatureTest feature) { 200 incFeatures; 201 string tagsDescription; 202 203 if(feature.tags.length) tagsDescription = format(" (%s)", feature.tags.join(", ")); 204 205 logfln("%s %s%s", "Feature:".color(fg.light_yellow), feature.name.color(fg.light_white, bg.init, mode.bold), tagsDescription); 206 ++indent; 207 208 if (feature.description.length) { 209 writeln(); 210 logln(feature.description); 211 writeln(); 212 } 213 214 feature.runBeforeAll; 215 216 logln("Scenarios:".color(fg.light_cyan)); 217 ++indent; // Indent the scenarios 218 foreach(scenario; feature.scenarios) { 219 bool scenarioPass = true; 220 221 feature.runBeforeEach; 222 logfln("%s".color(fg.light_white, bg.init, mode.bold), scenario.name); 223 ++indent; 224 try { 225 scenario.implementation(); 226 } 227 catch (Throwable t) { 228 string failMessage; 229 230 auto featureTestException = cast(FeatureTestException)t; 231 scenarioPass = false; 232 233 if (featureTestException && featureTestException.pending) { 234 pending ~= FeatureTestRunner.Failure(feature.name, scenario.name, t); 235 failMessage = "[ PENDING ]".color(fg.black, bg.light_yellow); 236 } 237 else { 238 failures ~= FeatureTestRunner.Failure(feature.name, scenario.name, t); 239 failMessage = "[ FAIL ]".color(fg.black, bg.light_red); 240 } 241 242 logln(failMessage); 243 244 // Rethrow the original error if it's not an AsserError or a FeatureTestException 245 if (!cast(AssertError)t && !featureTestException) throw t; 246 } 247 248 if (scenarioPass) { 249 logln("[ PASS ]".color(fg.black, bg.light_green)); 250 incPassed; 251 } 252 --indent; 253 feature.runAfterEach; 254 } 255 --indent; // Unindent the scenarios 256 feature.runAfterAll; 257 --indent; 258 writeln(); 259 } 260 261 private: 262 static FeatureTestRunner _runner; // The instance 263 264 // For display purposes 265 uint _indent; // Holds the current level of indentation 266 enum _tabString = " "; 267 uint _displayWidth; 268 bool randomize; 269 bool quiet; 270 271 @property string indentTabs() { 272 string tabs; 273 for(uint count = 0; count < _indent; ++count) tabs ~= _tabString; 274 return tabs; 275 } 276 } 277 }