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