1 module tests.monitor;
2 
3 import core.time;
4 import core.thread;
5 
6 import std.path;
7 import std.stdio;
8 import std.range;
9 import std.format;
10 import std.typecons;
11 import std.algorithm;
12 import std.experimental.logger;
13 
14 import fluent.asserts;
15 import trial.discovery.spec;
16 
17 import supervised;
18 
19 private alias suite = Spec!({
20 describe("ProcessMonitor", {
21     before({
22         import supervised.logging;
23 
24         writeln("Writing test logs to `tests.log`");
25         supervised.logging.logger = new FileLogger("tests.log", LogLevel.trace);
26     });
27 
28     it("handles multiple consecutive single short run processes", {
29         auto monitor = new shared ProcessMonitor;
30 
31         foreach (i; 0..100) {
32             monitor.running.should.equal(false).because("(At iteration %s)".format(i));
33 
34             auto count = 0;
35             auto others = 0;
36             monitor.stdoutCallback = (string message) @safe {
37                 if (message == "foo bar") count++;
38                 else others++;
39             };
40 
41             monitor.start(["echo", "foo bar"]);
42             monitor.running.should.equal(true).because("(At iteration %s)".format(i));
43             monitor.wait().should.equal(0).because("(At iteration %s)".format(i));
44 
45             count.should.equal(1).because("(At iteration %s)".format(i));
46             others.should.equal(0).because("(At iteration %s)".format(i));
47             monitor.running.should.equal(false).because("(At iteration %s)".format(i));
48         }
49     });
50 
51     it("handles passing messages to stdin, in order", {
52         auto monitor = new shared ProcessMonitor;
53 
54         string[] outputs;
55         monitor.stdoutCallback = (string message) @safe {
56             outputs ~= message;
57         };
58 
59         auto inputs = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
60                         .repeat(1000).join.array;
61 
62         monitor.start(["cat"]);
63         monitor.running.should.equal(true);
64 
65         foreach (input; inputs) {
66             monitor.send(input);
67         }
68 
69         monitor.closeStdin();
70         monitor.wait();
71 
72         outputs.should.equal(inputs);
73     });
74 
75     it("handles capturing stderr", {
76         auto monitor = new shared ProcessMonitor;
77 
78         string[] outputs;
79         monitor.stderrCallback = (string message) @safe {
80             outputs ~= message;
81         };
82 
83         auto inputs = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
84                         .repeat(1000).join.array;
85 
86         monitor.start(["python3", "tests/support/cat_stderr.py"]);
87         monitor.running.should.equal(true);
88 
89         foreach (input; inputs) {
90             monitor.send(input);
91         }
92 
93         monitor.closeStdin();
94         monitor.wait();
95 
96         outputs.should.equal(inputs);
97     });
98 
99     it("can kill a process that randomly dies", {
100         auto monitor = new shared ProcessMonitor;
101 
102         foreach (i; 0..20) {
103             monitor.running.should.equal(false);
104 
105             monitor.start(["python3", "tests/support/random_exit.py"]);
106             monitor.running.should.equal(true);
107 
108             Thread.sleep(500.msecs);
109             try {
110                 monitor.kill();
111             } catch (Exception e) {}
112             monitor.wait();
113 
114             monitor.running.should.equal(false);
115         }
116     });
117 
118     it("can kill a process that ignores SIGTERM", {
119         auto monitor = new shared ProcessMonitor;
120         monitor.killTimeout = 2.seconds;
121 
122         string[] outputs;
123         monitor.stdoutCallback = (string message) @safe {
124             outputs ~= message;
125         };
126         monitor.stderrCallback = (string message) @safe {
127             outputs ~= message;
128         };
129 
130         monitor.start(["python3", "tests/support/ignore_sigterm.py"]);
131         monitor.running.should.equal(true);
132 
133         // Give python some time to install the signal handler
134         Thread.sleep(200.msecs);
135 
136         monitor.kill();
137         monitor.send("foo");
138         monitor.running.should.equal(true);
139 
140         monitor.wait();
141 
142         monitor.running.should.equal(false);
143 
144         outputs.should.equal(["foo"]);
145     });
146 
147     it("handles a process that doesn't exit immediately", {
148         auto monitor = new shared ProcessMonitor;
149         monitor.killTimeout = 3.seconds;
150 
151         string[] outputs;
152         monitor.stdoutCallback = (string message) @safe {
153             outputs ~= message;
154         };
155 
156         monitor.start(["python3", "tests/support/late_exit.py"]);
157         monitor.running.should.equal(true);
158 
159         // Give python some time to install the signal handler
160         Thread.sleep(200.msecs);
161 
162         monitor.send("foo");
163         Thread.sleep(100.msecs);
164         monitor.kill();
165         monitor.running.should.equal(true);
166 
167         monitor.wait();
168 
169         monitor.running.should.equal(false);
170 
171         outputs.should.equal(["foo", "INTERRUPTED"]);
172     });
173 
174     it("handles different types of line endings", {
175         auto monitor = new shared ProcessMonitor;
176 
177         string[] outputs;
178         monitor.stdoutCallback = (string message) @safe {
179             outputs ~= message;
180         };
181 
182         monitor.start(["python3", "tests/support/line_endings.py"]);
183         monitor.running.should.equal(true);
184 
185         monitor.wait();
186 
187         monitor.running.should.equal(false);
188         outputs.should.equal(["linux", "osx", "shit"]);
189     });
190 
191     // TODO: Support this feature
192     /*it("handles processes that write directly to tty", {
193         auto monitor = new shared ProcessMonitor;
194 
195         string[] outputs;
196         monitor.stdoutCallback = (string message) @safe {
197             outputs ~= message;
198         };
199 
200         monitor.start(["python3", "tests/support/cat_tty.py"]);
201         monitor.running.should.equal(true);
202 
203         monitor.send("foo");
204 
205         Thread.sleep(100.msecs);
206         monitor.kill();
207         monitor.wait();
208 
209         outputs.should.equal(["foo"]);
210     });*/
211 
212     describe("this()", {
213         it("Starts a process immediately", {
214             auto monitor = new shared ProcessMonitor(["echo", "foo bar"]);
215             scope(exit) monitor.wait();
216 
217             monitor.running.should.equal(true);
218         });
219     });
220 
221     describe("start()", {
222         it("Starts process with proper environment", {
223             auto monitor = new shared ProcessMonitor;
224 
225             string[] outputs;
226             monitor.stdoutCallback = (string message) @safe {
227                 outputs ~= message;
228             };
229 
230             monitor.start(
231                 ["python3", "print_env.py"],
232                 [tuple("foo", "bar")],
233                 "tests/support"
234             );
235             monitor.wait();
236 
237             outputs.length.should.equal(3);
238             outputs[0].should.equal("['print_env.py']");
239             outputs[1].canFind("'foo': 'bar'").should.equal(true);
240             outputs[2].should.equal(absolutePath("tests/support"));
241         });
242 
243         it("Fails running a non-existent process", {
244             auto monitor = new shared ProcessMonitor;
245 
246             ({
247                 monitor.start(["tests/supports/this-file-does-not-exist"]);
248             }).should.throwException!ProcessException;
249         });
250 
251         it("Fails if a process is already running", {
252             auto monitor = new shared ProcessMonitor(["python3", "tests/support/print_on_exit.py"]);
253 
254             scope(exit) {
255                 monitor.kill();
256                 monitor.wait();
257             }
258 
259             ({
260                 monitor.start(["echo", "foo"]);
261             }).should.throwException!InvalidStateException;
262 
263         });
264     });
265 
266     describe("kill()", {
267         it("Fails if process is not running", {
268             auto monitor = new shared ProcessMonitor;
269 
270             ({
271                 monitor.kill(4);
272             }).should.throwException!InvalidStateException;
273         });
274     });
275 
276     describe("wait()", {
277         it("Returns the exit code", {
278             auto monitor = new shared ProcessMonitor(["python3", "tests/support/exit_code.py", "2"]);
279 
280             monitor.wait().should.equal(2);
281             monitor.wait().should.equal(2);
282 
283             monitor.start(["python3", "tests/support/exit_code.py", "0"]);
284 
285             monitor.wait().should.equal(0);
286             monitor.wait().should.equal(0);
287         });
288 
289         it("Handles being called without the process having started", {
290             auto monitor = new shared ProcessMonitor;
291 
292             monitor.wait().should.equal(0);
293         });
294     });
295 
296     describe("closeStdin()", {
297         it("Fails if process is not running", {
298             auto monitor = new shared ProcessMonitor;
299 
300             ({
301                 monitor.closeStdin();
302             }).should.throwException!InvalidStateException;
303         });
304     });
305 
306     describe("callbacks", {
307         it("Calls all callbacks", {
308             auto monitor = new shared ProcessMonitor;
309 
310             string[] stdout;
311             monitor.stdoutCallback = (string message) @safe {
312                 stdout ~= message;
313             };
314 
315             string[] stderr;
316             monitor.stderrCallback = (string message) @safe {
317                 stderr ~= message;
318             };
319 
320             auto terminations = 0;
321             monitor.terminateCallback = () @safe {
322                 terminations += 1;
323             };
324 
325             monitor.start(["bash", "-c", "echo foo >&2; echo bar"]);
326             monitor.wait().should.equal(0);
327 
328             stdout.should.equal(["bar"]);
329             stderr.should.equal(["foo"]);
330             terminations.should.equal(1);
331         });
332     });
333 });
334 });