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 }