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