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 }