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 });