1 /** 2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 */ 10 module dextool_test.builders; 11 12 import scriptlike; 13 14 import std.datetime.stopwatch : StopWatch; 15 import std.range : isInputRange; 16 import std.typecons : Yes, No, Flag; 17 import std.traits : ReturnType; 18 19 static import core.thread; 20 21 import dextool_test.utils : escapePath; 22 23 /** Build the command line arguments and working directory to use when invoking 24 * dextool. 25 */ 26 struct BuildDextoolRun { 27 import std.ascii : newline; 28 29 private { 30 string dextool; 31 string workdir_; 32 string test_outputdir; 33 string[] args_; 34 string[] post_args; 35 string[] flags_; 36 37 /// if the output from running the command should be yapped via scriptlike 38 bool yap_output = true; 39 40 /// if --debug is added to the arguments 41 bool arg_debug = true; 42 43 /// Throw an exception if the exit status is NOT zero 44 bool throw_on_exit_status = true; 45 } 46 47 /** 48 * Params: 49 * command = the executable to run 50 * workdir = directory to run the executable from 51 */ 52 this(string dextool, string workdir) { 53 this.dextool = dextool; 54 this.workdir_ = workdir; 55 this.test_outputdir = workdir; 56 } 57 58 Path workdir() { 59 return Path(workdir_); 60 } 61 62 auto setWorkdir(T)(T v) { 63 static if (is(T == string)) 64 workdir_ = v; 65 else 66 workdir_ = v.toString; 67 return this; 68 } 69 70 auto throwOnExitStatus(bool v) { 71 this.throw_on_exit_status = v; 72 return this; 73 } 74 75 auto flags(string[] v) { 76 this.flags_ = v; 77 return this; 78 } 79 80 auto addFlag(T)(T v) { 81 this.flags_ ~= v; 82 return this; 83 } 84 85 auto addDefineFlag(string v) { 86 this.flags_ ~= ["-D", v]; 87 return this; 88 } 89 90 auto addIncludeFlag(string v) { 91 this.flags_ ~= ["-I", v]; 92 return this; 93 } 94 95 auto addIncludeFlag(Path v) { 96 this.flags_ ~= ["-I", v.toString]; 97 return this; 98 } 99 100 auto args(string[] v) { 101 this.args_ = v; 102 return this; 103 } 104 105 auto addArg(T)(T v) { 106 this.args_ ~= v; 107 return this; 108 } 109 110 auto addArg(Path v) { 111 this.args_ ~= v.escapePath; 112 return this; 113 } 114 115 auto addInputArg(string v) { 116 post_args ~= "--in=" ~ Path(v).escapePath; 117 return this; 118 } 119 120 auto addInputArg(string[] v) { 121 post_args ~= v.map!(a => Path(a)).map!(a => "--in=" ~ a.escapePath).array(); 122 return this; 123 } 124 125 auto addInputArg(Path v) { 126 post_args ~= "--in=" ~ v.escapePath; 127 return this; 128 } 129 130 auto addInputArg(Path[] v) { 131 post_args ~= v.map!(a => "--in=" ~ a.escapePath).array(); 132 return this; 133 } 134 135 auto postArg(string[] v) { 136 this.post_args = v; 137 return this; 138 } 139 140 auto addPostArg(T)(T v) { 141 this.post_args ~= v; 142 return this; 143 } 144 145 auto addPostArg(Path v) { 146 this.post_args ~= v.escapePath; 147 return this; 148 } 149 150 /// Activate debugging mode of the dextool binary 151 auto argDebug(bool v) { 152 arg_debug = v; 153 return this; 154 } 155 156 auto yapOutput(bool v) { 157 yap_output = v; 158 return this; 159 } 160 161 auto run() { 162 import std.array : join; 163 import std.algorithm : min; 164 165 string[] cmd; 166 cmd ~= dextool; 167 cmd ~= args_.dup; 168 cmd ~= post_args; 169 cmd ~= "--out=" ~ workdir_; 170 171 if (arg_debug) { 172 cmd ~= "--debug"; 173 } 174 175 if (flags_.length > 0) { 176 cmd ~= "--"; 177 cmd ~= flags_.dup; 178 } 179 180 StopWatch sw; 181 ReturnType!(std.process.tryWait) exit_; 182 exit_.status = -1; 183 Appender!(string[]) stdout_; 184 Appender!(string[]) stderr_; 185 186 sw.start; 187 try { 188 auto p = std.process.pipeProcess(cmd, 189 std.process.Redirect.stdout | std.process.Redirect.stderr); 190 191 for (;;) { 192 exit_ = std.process.tryWait(p.pid); 193 194 foreach (l; p.stdout.byLineCopy) 195 stdout_.put(l); 196 foreach (l; p.stderr.byLineCopy) 197 stderr_.put(l); 198 199 if (exit_.terminated) 200 break; 201 core.thread.Thread.sleep(20.msecs); 202 } 203 sw.stop; 204 } 205 catch (Exception e) { 206 stderr_ ~= [e.msg]; 207 sw.stop; 208 } 209 210 auto rval = BuildCommandRunResult(exit_.status == 0, exit_.status, 211 stdout_.data, stderr_.data, sw.peek.total!"msecs", cmd); 212 if (yap_output) { 213 auto f = File(nextFreeLogfile(test_outputdir), "w"); 214 f.writef("%s", rval); 215 } 216 217 if (throw_on_exit_status && exit_.status != 0) { 218 auto l = min(10, stderr_.data.length); 219 throw new ErrorLevelException(exit_.status, stderr_.data[0 .. l].join(newline)); 220 } else { 221 return rval; 222 } 223 } 224 } 225 226 /** Build the command line arguments and working directory to use when invoking 227 * a command. 228 */ 229 struct BuildCommandRun { 230 import std.ascii : newline; 231 232 private { 233 string command; 234 string workdir_; 235 string[] args_; 236 237 bool run_in_outdir; 238 239 /// if the output from running the command should be yapped via scriptlike 240 bool yap_output = true; 241 242 /// Throw an exception if the exit status is NOT zero 243 bool throw_on_exit_status = true; 244 } 245 246 this(string command) { 247 this.command = command; 248 run_in_outdir = false; 249 } 250 251 /** 252 * Params: 253 * command = the executable to run 254 * workdir = directory to run the executable from 255 */ 256 this(string command, string workdir) { 257 this.command = command; 258 this.workdir_ = workdir; 259 run_in_outdir = true; 260 } 261 262 Path workdir() { 263 return Path(workdir_); 264 } 265 266 auto setWorkdir(Path v) { 267 workdir_ = v.toString; 268 return this; 269 } 270 271 /// If the command to run is in workdir. 272 auto commandInOutdir(bool v) { 273 run_in_outdir = v; 274 return this; 275 } 276 277 auto throwOnExitStatus(bool v) { 278 this.throw_on_exit_status = v; 279 return this; 280 } 281 282 auto args(string[] v) { 283 this.args_ = v; 284 return this; 285 } 286 287 auto addArg(string v) { 288 this.args_ ~= v; 289 return this; 290 } 291 292 auto addArg(Path v) { 293 this.args_ ~= v.escapePath; 294 return this; 295 } 296 297 auto addArg(string[] v) { 298 this.args_ ~= v; 299 return this; 300 } 301 302 auto addFileFromOutdir(string v) { 303 this.args_ ~= buildPath(workdir_, v); 304 return this; 305 } 306 307 auto yapOutput(bool v) { 308 yap_output = v; 309 return this; 310 } 311 312 auto run() { 313 import std.path : buildPath; 314 315 string[] cmd; 316 if (run_in_outdir) 317 cmd ~= buildPath(workdir.toString, command); 318 else 319 cmd ~= command; 320 cmd ~= args_.dup; 321 322 StopWatch sw; 323 ReturnType!(std.process.tryWait) exit_; 324 exit_.status = -1; 325 Appender!(string[]) stdout_; 326 Appender!(string[]) stderr_; 327 328 sw.start; 329 try { 330 auto p = std.process.pipeProcess(cmd, 331 std.process.Redirect.stdout | std.process.Redirect.stderr); 332 333 for (;;) { 334 exit_ = std.process.tryWait(p.pid); 335 336 foreach (l; p.stdout.byLineCopy) 337 stdout_.put(l); 338 foreach (l; p.stderr.byLineCopy) 339 stderr_.put(l); 340 341 if (exit_.terminated) 342 break; 343 core.thread.Thread.sleep(10.msecs); 344 } 345 346 sw.stop; 347 } 348 catch (Exception e) { 349 stderr_ ~= [e.msg]; 350 sw.stop; 351 } 352 353 auto rval = BuildCommandRunResult(exit_.status == 0, exit_.status, 354 stdout_.data, stderr_.data, sw.peek.total!"msecs", cmd); 355 if (yap_output) { 356 auto f = File(nextFreeLogfile(workdir_), "w"); 357 f.writef("%s", rval); 358 } 359 360 if (throw_on_exit_status && exit_.status != 0) { 361 auto l = min(10, stderr_.data.length); 362 throw new ErrorLevelException(exit_.status, stderr_.data[0 .. l].join(newline)); 363 } else { 364 return rval; 365 } 366 } 367 } 368 369 private auto nextFreeLogfile(string workdir) { 370 import std.file : exists; 371 import std.path : baseName, buildPath; 372 import std..string : format; 373 374 int idx; 375 string f; 376 do { 377 f = buildPath(workdir, format("run_command%s.log", idx)); 378 ++idx; 379 } 380 while (exists(f)); 381 382 return f; 383 } 384 385 struct BuildCommandRunResult { 386 import std.ascii : newline; 387 import std.format : FormatSpec; 388 389 /// convenient value which is true when exit status is zero. 390 const bool success; 391 /// actual exit status 392 const int status; 393 /// captured output 394 string[] stdout; 395 string[] stderr; 396 /// time to execute the command. TODO: change to Duration after DMD v2.076 397 const long executionMsecs; 398 399 private string[] cmd; 400 401 void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const { 402 import std.algorithm : joiner; 403 import std.format : formattedWrite; 404 import std.range.primitives : put; 405 406 formattedWrite(w, "run: %s", cmd.dup.joiner(" ")); 407 put(w, newline); 408 409 formattedWrite(w, "exit status: %s", status); 410 put(w, newline); 411 formattedWrite(w, "execution time ms: %s", executionMsecs); 412 put(w, newline); 413 414 put(w, "stdout:"); 415 put(w, newline); 416 this.stdout.each!((a) { put(w, a); put(w, newline); }); 417 418 put(w, "stderr:"); 419 put(w, newline); 420 this.stderr.each!((a) { put(w, a); put(w, newline); }); 421 } 422 423 string toString() @safe pure const { 424 import std.exception : assumeUnique; 425 import std.format : FormatSpec; 426 427 char[] buf; 428 buf.reserve(100); 429 auto fmt = FormatSpec!char("%s"); 430 toString((const(char)[] s) { buf ~= s; }, fmt); 431 auto trustedUnique(T)(T t) @trusted { 432 return assumeUnique(t); 433 } 434 435 return trustedUnique(buf); 436 } 437 }