1 /**
2 	Unit test runner with structured result output support.
3 
4 	Copyright: Copyright ©2013 rejectedsoftware e.K., all rights reserved.
5 	Authors: Sönke Ludwig
6 */
7 module tested;
8 
9 import core.sync.condition;
10 import core.sync.mutex;
11 import core.memory;
12 import core.time;
13 import core.thread;
14 import std.datetime.stopwatch : StopWatch;
15 import std.string : startsWith;
16 import std.traits;
17 import std.typetuple : TypeTuple;
18 
19 
20 /**
21 	Runs all unit tests contained in the given symbol recursively.
22 
23 	COMPOSITES can be a list of modules or composite types.
24 
25 	Example:
26 		This example assumes that the application has all of its sources in
27 		the package named "mypackage". All contained tests will be run
28 		recursively.
29 
30 		---
31 		import tested;
32 		import mypackage.application;
33 
34 		void main()
35 		{
36 			version(unittest) runUnitTests!(mypackage.application)(new ConsoleResultWriter);
37 			else runApplication;
38 		}
39 		---
40 */
41 bool runUnitTests(COMPOSITES...)(TestResultWriter results)
42 {
43 	assert(!g_runner, "Running multiple unit tests concurrently is not supported.");
44 	g_runner = new TestRunner(results);
45 	auto ret = g_runner.runUnitTests!COMPOSITES();
46 	g_runner = null;
47 	return ret;
48 }
49 
50 
51 /**
52 	Emits a custom instrumentation value that will be stored in the unit test results.
53 */
54 void instrument(string name, double value)
55 {
56 	if (g_runner) g_runner.instrument(name, value);
57 }
58 
59 
60 /**
61 	Attribute for giving a unit test a name.
62 
63 	The name of a unit test will be output in addition to the fully
64 	qualified name of the test function in the test output.
65 
66 	Example:
67 		---
68 		@name("some test")
69 		unittest {
70 			// unit test code
71 		}
72 
73 		@name("some other test")
74 		unittest {
75 			// unit test code
76 		}
77 		---	
78 */
79 struct name { string name; }
80 
81 
82 /**
83 	Base interface for all unit test result writers.
84 */
85 interface TestResultWriter {
86 	void finalize();
87 	void beginTest(string name, string qualified_name);
88 	void addScalar(Duration timestamp, string name, double value);
89 	void endTest(Duration timestamp, Throwable error);
90 }
91 
92 
93 /**
94 	Directly outputs unit test results to the console.
95 */
96 class ConsoleTestResultWriter : TestResultWriter {
97 	import std.stdio;
98 	private {
99 		size_t m_failCount, m_successCount;
100 		string m_name, m_qualifiedName;
101 	}
102 
103 	void finalize()
104 	{
105 		writefln("===========================");
106 		writefln("%s of %s tests have passed.", m_successCount, m_successCount+m_failCount);
107 		writefln("");
108 		writefln("FINAL RESULT: %s", m_failCount > 0 ? "FAILED" : "PASSED");
109 	}
110 
111 	void beginTest(string name, string qualified_name)
112 	{
113 		m_name = name;
114 		m_qualifiedName = qualified_name;
115 	}
116 
117 	void addScalar(Duration timestamp, string name, double value)
118 	{
119 	}
120 
121 	void endTest(Duration timestamp, Throwable error)
122 	{
123 		if (error) {
124 			version(Posix) write("\033[1;31m");
125 			writefln(`FAIL "%s" (%s) after %.6f s: %s`, m_name, m_qualifiedName, fracSecs(timestamp), error.msg);
126 			version(Posix) write("\033[0m");
127 			m_failCount++;
128 		} else {
129 			writefln(`PASS "%s" (%s) after %.6f s`, m_name, m_qualifiedName, fracSecs(timestamp));
130 			m_successCount++;
131 		}
132 	}
133 
134 	static double fracSecs(Duration dur)
135 	{
136 		return 1E-6 * dur.total!"usecs";
137 	}
138 }
139 
140 /**
141 	Outputs test results and instrumentation values 
142 */
143 class JsonTestResultWriter : TestResultWriter {
144 	import std.stdio;
145 
146 	private {
147 		File m_file;
148 		bool m_gotInstrumentValue, m_gotUnitTest;
149 	}
150 
151 	this(string filename)
152 	{
153 		m_file = File(filename, "w+b");
154 		m_file.writeln("[");
155 		m_gotUnitTest = false;
156 	}
157 
158 	void finalize()
159 	{
160 		m_file.writeln();
161 		m_file.writeln("]");
162 		m_file.close();
163 	}
164 
165 	void beginTest(string name, string qualified_name)
166 	{
167 		if (m_gotUnitTest) m_file.writeln(",");
168 		else m_gotUnitTest = true;
169 		m_file.writef(`{"name": "%s", "qualifiedName": "%s", "instrumentation": [`, name, qualified_name);
170 		m_gotInstrumentValue = false;
171 	}
172 
173 	void addScalar(Duration timestamp, string name, double value)
174 	{
175 		if (m_gotInstrumentValue) m_file.write(", ");
176 		else m_gotInstrumentValue = true;
177 		m_file.writef(`{"name": "%s", "value": %s, "timestamp": %s}`, name, value, timestamp.total!"usecs" * 1E-6);
178 	}
179 
180 	void endTest(Duration timestamp, Throwable error)
181 	{
182 		m_file.writef(`], "success": %s, "duration": %s`, error is null, timestamp.total!"usecs" * 1E-6);
183 		if (error) m_file.writef(`, "message": "%s"`, error.msg);
184 		m_file.write("}");
185 	}
186 }
187 
188 private class TestRunner {
189 	private {
190 		Mutex m_mutex;
191 		Condition m_condition;
192 		TestResultWriter m_results;
193 		InstrumentStats m_baseStats;
194 		bool m_running, m_quit, m_instrumentsReady;
195 		StopWatch m_stopWatch;
196 	}
197 
198 	this(TestResultWriter writer)
199 	{
200 		m_results = writer;
201 		m_mutex = new Mutex;
202 		m_condition = new Condition(m_mutex);
203 	}
204 
205 	bool runUnitTests(COMPOSITES...)()
206 	{
207 		InstrumentStats basestats;
208 		m_running = false;
209 		m_quit = false;
210 		m_instrumentsReady = false;
211 
212 		auto instrumentthr = new Thread(&instrumentThread);
213 		instrumentthr.name = "instrumentation thread";
214 		//instrumentthr.priority = Thread.PRIORITY_DEFAULT + 1;
215 		instrumentthr.start();
216 
217 		bool[string] visitedMembers;
218 		auto ret = true;
219 		foreach(comp; COMPOSITES)
220 			if (!runUnitTestsImpl!comp(visitedMembers))
221 				ret = false;
222 		m_results.finalize();
223 
224 		synchronized (m_mutex) m_quit = true;
225 		m_condition.notifyAll();
226 		instrumentthr.join();
227 
228 		return ret;
229 	}
230 
231 	private void instrument(string name, double value)
232 	{
233 		auto ts = cast(Duration)m_stopWatch.peek();
234 		synchronized (m_mutex) m_results.addScalar(ts, name, value);
235 	}
236 
237 	private bool runUnitTestsImpl(COMPOSITE...)(ref bool[string] visitedMembers)
238 		if (COMPOSITE.length == 1 && isUnitTestContainer!COMPOSITE)
239 	{
240 		bool ret = true;
241 		//pragma(msg, fullyQualifiedName!COMPOSITE);
242 
243 		foreach (test; __traits(getUnitTests, COMPOSITE)) {
244 			if (!runUnitTest!test())
245 				ret = false;
246 		}
247 		// if COMPOSITE has members, descent recursively
248 		static if (isUnitTestContainer!COMPOSITE) {
249 			foreach (M; __traits(allMembers, COMPOSITE)) {
250 				static if (
251 					__traits(compiles, __traits(getMember, COMPOSITE, M)) &&
252 					isSingleField!(__traits(getMember, COMPOSITE, M)) &&
253 					isUnitTestContainer!(__traits(getMember, COMPOSITE, M)) &&
254 					!isModule!(__traits(getMember, COMPOSITE, M))
255 					)
256 				{
257 					// Don't visit the same member again.
258 					// This can be checked at compile time, but it's easier and much much
259 					// faster at runtime.
260 					if (__traits(getMember, COMPOSITE, M).mangleof !in visitedMembers)
261 					{ 
262 						visitedMembers[__traits(getMember, COMPOSITE, M).mangleof] = true;
263 						if (!runUnitTestsImpl!(__traits(getMember, COMPOSITE,	M))(visitedMembers))
264 							ret = false;
265 					}
266 				}
267 			}
268 		}
269 
270 		return ret;
271 	}
272 
273 	private bool runUnitTest(alias test)()
274 	{
275 		string name;
276 		foreach (att; __traits(getAttributes, test))
277 			static if (is(typeof(att) == .name))
278 				name = att.name;
279 
280 		// wait for instrumentation thread to get ready
281 		synchronized (m_mutex)
282 			while (!m_instrumentsReady)
283 				m_condition.wait();
284 
285 		m_results.beginTest(name, fullyQualifiedName!test);
286 		m_stopWatch.reset();
287 
288 		GC.collect();
289 		m_baseStats = InstrumentStats.instrument();
290 
291 		synchronized (m_mutex) m_running = true;
292 		m_condition.notifyAll();
293 
294 		Throwable error;
295 		try {
296 			m_stopWatch.start();
297 			test();
298 			m_stopWatch.stop();
299 		} catch (Throwable th) {
300 			m_stopWatch.stop();
301 			error = th;
302 		}
303 
304 		auto duration = cast(Duration)m_stopWatch.peek();
305 
306 		auto stats = InstrumentStats.instrument();
307 		synchronized (m_mutex) {
308 			writeInstrumentStats(m_results, duration, m_baseStats, stats);
309 			m_running = false;
310 		}
311 			
312 		m_results.endTest(duration, error);
313 		return error is null;
314 	}
315 
316 	private void instrumentThread()
317 	{
318 		while (true) {
319 			synchronized (m_mutex) {
320 				if (m_quit) return;
321 				if (!m_running) {
322 					m_instrumentsReady = true;
323 					m_condition.notifyAll();
324 					while (!m_running && !m_quit) m_condition.wait();
325 					if (m_quit) return;
326 					m_instrumentsReady = false;
327 				}
328 			}
329 
330 			Thread.sleep(10.msecs);
331 
332 			auto ts = cast(Duration)m_stopWatch.peek;
333 			auto stats = InstrumentStats.instrument();
334 			synchronized (m_mutex)
335 				if (m_running)
336 					writeInstrumentStats(m_results, ts, m_baseStats, stats);
337 		}
338 	}
339 }
340 
341 
342 // copied from gc.stats
343 private struct GCStats {
344     size_t poolsize;        // total size of pool
345     size_t usedsize;        // bytes allocated
346     size_t freeblocks;      // number of blocks marked FREE
347     size_t freelistsize;    // total of memory on free lists
348     size_t pageblocks;      // number of blocks marked PAGE
349 }
350 
351 // from gc.proxy
352 private extern (C) GCStats gc_stats();
353 
354 private struct InstrumentStats {
355 	size_t gcPoolSize;
356 	size_t gcUsedSize;
357 	size_t gcFreeListSize;
358 
359 	static InstrumentStats instrument()
360 	{
361 		auto stats = gc_stats();
362 		InstrumentStats ret;
363 		ret.gcPoolSize = stats.poolsize;
364 		ret.gcUsedSize = stats.usedsize;
365 		ret.gcFreeListSize = stats.freelistsize;
366 		return ret;
367 	}
368 }
369 
370 private {
371 	__gshared TestRunner g_runner;
372 }
373 
374 private void writeInstrumentStats(TestResultWriter results, Duration timestamp, InstrumentStats base_stats, InstrumentStats stats)
375 {
376 	results.addScalar(timestamp, "gcPoolSize", stats.gcPoolSize - base_stats.gcPoolSize);
377 	results.addScalar(timestamp, "gcUsedSize", stats.gcUsedSize - base_stats.gcUsedSize);
378 	results.addScalar(timestamp, "gcFreeListSize", stats.gcFreeListSize - base_stats.gcFreeListSize);
379 }
380 
381 private template isUnitTestContainer(DECL...)
382 	if (DECL.length == 1)
383 {
384 	static if (!isAccessible!DECL) {
385 		enum isUnitTestContainer = false;
386 	} else static if (is(FunctionTypeOf!(DECL[0]))) {
387 		enum isUnitTestContainer = false;
388 	} else static if (is(DECL[0]) && !isAggregateType!(DECL[0])) {
389 		enum isUnitTestContainer = false;
390 	} else static if (isPackage!(DECL[0])) {
391 		enum isUnitTestContainer = false;
392 	} else static if (isModule!(DECL[0])) {
393 		enum isUnitTestContainer = DECL[0].stringof != "module object";
394 	} else static if (!__traits(compiles, fullyQualifiedName!(DECL[0]))) {
395 		enum isUnitTestContainer = false;
396 	} else static if (!is(typeof(__traits(allMembers, DECL[0])))) {
397 		enum isUnitTestContainer = false;
398 	} else {
399 		enum isUnitTestContainer = true;
400 	}
401 }
402 
403 private template isModule(DECL...)
404 	if (DECL.length == 1)
405 {
406 	static if (is(DECL[0])) enum isModule = false;
407 	else static if (is(typeof(DECL[0])) && !is(typeof(DECL[0]) == void)) enum isModule = false;
408 	else static if (!is(typeof(DECL[0].stringof))) enum isModule = false;
409 	else static if (is(FunctionTypeOf!(DECL[0]))) enum isModule = false;
410 	else enum isModule = DECL[0].stringof.startsWith("module ");
411 }
412 
413 private template isPackage(DECL...)
414 	if (DECL.length == 1)
415 {
416 	static if (is(DECL[0])) enum isPackage = false;
417 	else static if (is(typeof(DECL[0])) && !is(typeof(DECL[0]) == void)) enum isPackage = false;
418 	else static if (!is(typeof(DECL[0].stringof))) enum isPackage = false;
419 	else static if (is(FunctionTypeOf!(DECL[0]))) enum isPackage = false;
420 	else enum isPackage = DECL[0].stringof.startsWith("package ");
421 }
422 
423 private template isAccessible(DECL...)
424 	if (DECL.length == 1)
425 {
426 	enum isAccessible = __traits(compiles, testTempl!(DECL[0])());
427 }
428 
429 private template isSingleField(DECL...)
430 {
431 	enum isSingleField = DECL.length == 1;
432 }
433 
434 
435 static assert(!is(tested));
436 static assert(isModule!tested);
437 static assert(!isPackage!tested);
438 static assert(isPackage!std);
439 static assert(__traits(compiles, testTempl!GCStats()));
440 static assert(__traits(compiles, testTempl!(immutable(ubyte)[])));
441 static assert(isAccessible!GCStats);
442 static assert(isUnitTestContainer!GCStats);
443 static assert(isUnitTestContainer!tested);
444 
445 private void testTempl(X...)()
446 	if (X.length == 1)
447 {
448 	static if (is(X[0])) {
449 		auto x = X[0].init;
450 	} else {
451 		auto x = X[0].stringof;
452 	}
453 }