1 /** 2 * IO related functions 3 */ 4 5 module unit_threaded.runner.io; 6 7 import unit_threaded.from; 8 9 /** 10 * Write if debug output was enabled. 11 */ 12 void writelnUt(T...)(auto ref T args) { 13 debug { 14 import unit_threaded.runner.testcase: TestCase; 15 if(isDebugOutputEnabled) 16 TestCase.currentTest.getWriter.writeln(args); 17 } 18 } 19 20 21 22 private shared(bool) _debugOutput = false; ///print debug msgs? 23 private shared(bool) _forceEscCodes = false; ///use ANSI escape codes anyway? 24 private shared(bool) _useEscCodes; 25 enum _escCodes = ["\033[1;31m", "\033[1;32m", "\033[1;33m", "\033[0m"]; 26 27 28 29 version (Windows) { 30 import core.sys.windows.winbase: GetStdHandle, STD_OUTPUT_HANDLE, INVALID_HANDLE_VALUE; 31 import core.sys.windows.wincon: GetConsoleMode, SetConsoleMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING; 32 33 private __gshared uint originalConsoleMode; 34 } 35 36 private extern (C) int isatty(int) nothrow; // POSIX, MSVC and DigitalMars C runtime 37 38 shared static this() { 39 import std.stdio: stdout; 40 41 _useEscCodes = _forceEscCodes || isatty(stdout.fileno()) != 0; 42 43 // Windows: if _useEscCodes == true, enable ANSI escape codes for the stdout console 44 // (supported since Win10 v1511) 45 version (Windows) { 46 if (!_useEscCodes) 47 return; 48 49 auto handle = GetStdHandle(STD_OUTPUT_HANDLE); 50 bool success = handle && handle != INVALID_HANDLE_VALUE; 51 52 if (success && !GetConsoleMode(handle, &originalConsoleMode)) 53 success = false; 54 if (success && !(originalConsoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { 55 if (!SetConsoleMode(handle, originalConsoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) 56 success = false; 57 } 58 59 if (!success) 60 _useEscCodes = false; 61 } 62 } 63 64 // Windows: restore original console mode on shutdown 65 version (Windows) { 66 shared static ~this() { 67 if (_useEscCodes && !(originalConsoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)) 68 SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), originalConsoleMode); 69 } 70 } 71 72 73 void enableDebugOutput(bool value = true) nothrow { 74 synchronized { 75 _debugOutput = value; 76 } 77 } 78 79 package bool isDebugOutputEnabled() nothrow @trusted { 80 synchronized { 81 return _debugOutput; 82 } 83 } 84 85 package void forceEscCodes() nothrow { 86 synchronized { 87 _forceEscCodes = true; 88 } 89 } 90 91 interface Output { 92 void send(in string output) @safe; 93 void flush() @safe; 94 } 95 96 private enum Colour { 97 red, 98 green, 99 yellow, 100 cancel, 101 } 102 103 private string colour(alias C)(in string msg) { 104 return escCode(C) ~ msg ~ escCode(Colour.cancel); 105 } 106 107 private alias green = colour!(Colour.green); 108 private alias red = colour!(Colour.red); 109 private alias yellow = colour!(Colour.yellow); 110 111 /** 112 * Send escape code to the console 113 */ 114 private string escCode(in Colour code) @safe { 115 return _useEscCodes ? _escCodes[code] : ""; 116 } 117 118 119 /** 120 * Writes the args in a thread-safe manner. 121 */ 122 void write(T...)(Output output, auto ref T args) { 123 import std.conv: text; 124 output.send(text(args)); 125 } 126 127 /** 128 * Writes the args in a thread-safe manner and appends a newline. 129 */ 130 void writeln(T...)(Output output, auto ref T args) { 131 write(output, args, "\n"); 132 } 133 134 /** 135 * Writes the args in a thread-safe manner in green (POSIX only). 136 * and appends a newline. 137 */ 138 void writelnGreen(T...)(Output output, auto ref T args) { 139 import std.conv: text; 140 output.send(green(text(args) ~ "\n")); 141 } 142 143 /** 144 * Writes the args in a thread-safe manner in red (POSIX only) 145 * and appends a newline. 146 */ 147 void writelnRed(T...)(Output output, auto ref T args) { 148 writeRed(output, args, "\n"); 149 } 150 151 /** 152 * Writes the args in a thread-safe manner in red (POSIX only). 153 * and appends a newline. 154 */ 155 void writeRed(T...)(Output output, auto ref T args) { 156 import std.conv: text; 157 output.send(red(text(args))); 158 } 159 160 /** 161 * Writes the args in a thread-safe manner in yellow (POSIX only). 162 * and appends a newline. 163 */ 164 void writeYellow(T...)(Output output, auto ref T args) { 165 import std.conv: text; 166 output.send(yellow(text(args))); 167 } 168 169 /** 170 * Thread to output to stdout 171 */ 172 class WriterThread: Output { 173 174 import std.concurrency: Tid; 175 176 177 /** 178 * Returns a reference to the only instance of this class. 179 */ 180 static WriterThread get() @trusted { 181 import std.concurrency: initOnce; 182 static __gshared WriterThread instance; 183 return initOnce!instance(new WriterThread); 184 } 185 186 override void send(in string output) @safe { 187 188 version(unitUnthreaded) { 189 import std.stdio: write; 190 write(output); 191 } else { 192 import std.concurrency: send, thisTid; 193 () @trusted { _tid.send(output, thisTid); }(); 194 } 195 } 196 197 override void flush() @safe { 198 version(unitUnthreaded) {} 199 else { 200 import std.concurrency: send, thisTid; 201 () @trusted { _tid.send(Flush(), thisTid); }(); 202 } 203 } 204 205 206 private: 207 208 this() { 209 version(unitUnthreaded) {} 210 else { 211 import std.concurrency: spawn, thisTid, receiveOnly, send; 212 import std.stdio: stdout, stderr; 213 _tid = spawn(&threadWriter!(stdout, stderr), thisTid); 214 _tid.send(ThreadWait()); 215 receiveOnly!ThreadStarted; 216 } 217 } 218 219 220 Tid _tid; 221 } 222 223 224 struct ThreadWait{}; 225 struct ThreadFinish{}; 226 struct ThreadStarted{}; 227 struct ThreadEnded{}; 228 struct Flush{}; 229 230 version (Posix) { 231 enum nullFileName = "/dev/null"; 232 } else { 233 enum nullFileName = "NUL"; 234 } 235 236 237 void threadWriter(alias OUT, alias ERR)(from!"std.concurrency".Tid tid) 238 { 239 import std.concurrency: receive, send, OwnerTerminated, Tid; 240 241 auto done = false; 242 243 auto saveStdout = OUT; 244 auto saveStderr = ERR; 245 246 void restore() { 247 saveStdout.flush(); 248 OUT = saveStdout; 249 ERR = saveStderr; 250 } 251 252 scope (failure) restore; 253 254 if (!isDebugOutputEnabled()) { 255 OUT = typeof(OUT)(nullFileName, "w"); 256 ERR = typeof(ERR)(nullFileName, "w"); 257 } 258 259 void actuallyPrint(in string msg) { 260 if(msg.length) saveStdout.write(msg); 261 } 262 263 // the first thread to send output becomes the current 264 // until that thread sends a Flush message no other thread 265 // can print to stdout, so we store their outputs in the meanwhile 266 static struct ThreadOutput { 267 string currentOutput; 268 string[] outputs; 269 270 void store(in string msg) { 271 currentOutput ~= msg; 272 } 273 274 void flush() { 275 outputs ~= currentOutput; 276 currentOutput = ""; 277 } 278 } 279 ThreadOutput[Tid] outputs; 280 281 Tid currentTid; 282 283 while (!done) { 284 receive( 285 (string msg, Tid originTid) { 286 287 if(currentTid == currentTid.init) { 288 currentTid = originTid; 289 290 // it could be that this thread became the current thread but had output not yet printed 291 if(originTid in outputs) { 292 actuallyPrint(outputs[originTid].currentOutput); 293 outputs[originTid].currentOutput = ""; 294 } 295 } 296 297 if(currentTid == originTid) 298 actuallyPrint(msg); 299 else { 300 if(originTid !in outputs) outputs[originTid] = typeof(outputs[originTid]).init; 301 outputs[originTid].store(msg); 302 } 303 }, 304 (ThreadWait w) { 305 tid.send(ThreadStarted()); 306 }, 307 (ThreadFinish f) { 308 done = true; 309 }, 310 (Flush f, Tid originTid) { 311 312 if(originTid in outputs) outputs[originTid].flush; 313 314 if(currentTid != currentTid.init && currentTid != originTid) 315 return; 316 317 foreach(_, ref threadOutput; outputs) { 318 foreach(o; threadOutput.outputs) 319 actuallyPrint(o); 320 threadOutput.outputs = []; 321 } 322 323 currentTid = currentTid.init; 324 }, 325 (OwnerTerminated trm) { 326 done = true; 327 } 328 ); 329 } 330 331 restore; 332 tid.send(ThreadEnded()); 333 }