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 }