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 }