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