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 }