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 }