Make run() return ReturnObject() instances
Markus Gerstel
4 years ago
346 | 346 | |
347 | 347 | |
348 | 348 | class ReturnObject(dict, _ReturnObjectParent): |
349 | """ | |
350 | A subprocess.CompletedProcess-like object containing the executed | |
351 | command, stdout and stderr (both as bytestrings), and the exitcode. | |
352 | Further values such as process runtime can be accessed as dictionary | |
353 | values. | |
354 | The check_returncode() function raises an exception if the process | |
355 | exited with a non-zero exit code. | |
356 | """ | |
357 | ||
349 | 358 | def __init__(self, *arg, **kw): |
350 | 359 | super(ReturnObject, self).__init__(*arg, **kw) |
351 | 360 | self.args = self["command"] |
393 | 402 | extension. |
394 | 403 | :param string working_directory: If specified, run the executable from |
395 | 404 | within this working directory. |
396 | :return: A dictionary containing stdout, stderr (both as bytestrings), | |
397 | runtime, exitcode, and more. | |
405 | :return: A ReturnObject() containing the executed command, stdout and stderr | |
406 | (both as bytestrings), and the exitcode. Further values such as | |
407 | process runtime can be accessed as dictionary values. | |
398 | 408 | """ |
399 | 409 | |
400 | 410 | time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime()) |
542 | 552 | stderr = stderr.get_output() |
543 | 553 | time_end = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime()) |
544 | 554 | |
545 | result = { | |
546 | "exitcode": p.returncode, | |
547 | "command": command, | |
548 | "stdout": stdout, | |
549 | "stderr": stderr, | |
550 | "timeout": timeout_encountered, | |
551 | "runtime": runtime, | |
552 | "time_start": time_start, | |
553 | "time_end": time_end, | |
554 | } | |
555 | result = ReturnObject( | |
556 | { | |
557 | "exitcode": p.returncode, | |
558 | "command": command, | |
559 | "stdout": stdout, | |
560 | "stderr": stderr, | |
561 | "timeout": timeout_encountered, | |
562 | "runtime": runtime, | |
563 | "time_start": time_start, | |
564 | "time_end": time_end, | |
565 | } | |
566 | ) | |
555 | 567 | if stdin is not None: |
556 | 568 | result.update( |
557 | 569 | { |
575 | 587 | time_start = time.strftime("%Y-%m-%d %H:%M:%S GMT", time.gmtime()) |
576 | 588 | logger.info("run_process is disabled. Requested command: %s", command) |
577 | 589 | |
578 | result = { | |
579 | "exitcode": 0, | |
580 | "command": command, | |
581 | "stdout": "", | |
582 | "stderr": "", | |
583 | "timeout": False, | |
584 | "runtime": 0, | |
585 | "time_start": time_start, | |
586 | "time_end": time_start, | |
587 | } | |
590 | result = ReturnObject( | |
591 | { | |
592 | "exitcode": 0, | |
593 | "command": command, | |
594 | "stdout": "", | |
595 | "stderr": "", | |
596 | "timeout": False, | |
597 | "runtime": 0, | |
598 | "time_start": time_start, | |
599 | "time_end": time_start, | |
600 | } | |
601 | ) | |
588 | 602 | if kwargs.get("stdin") is not None: |
589 | 603 | result.update( |
590 | 604 | {"stdin_bytes_sent": len(kwargs["stdin"]), "stdin_bytes_remain": 0} |
93 | 93 | ) |
94 | 94 | assert not mock_process.terminate.called |
95 | 95 | assert not mock_process.kill.called |
96 | assert actual == expected | |
96 | for key in expected: | |
97 | assert actual[key] == expected[key] | |
98 | assert actual.args == tuple(command) | |
99 | assert actual.returncode == mock_process.returncode | |
100 | assert actual.stdout == mock.sentinel.proc_stdout | |
101 | assert actual.stderr == mock.sentinel.proc_stderr | |
97 | 102 | |
98 | 103 | |
99 | 104 | @mock.patch("procrunner.subprocess") |
14 | 14 | |
15 | 15 | result = procrunner.run(command) |
16 | 16 | |
17 | assert result["exitcode"] == 0 | |
18 | assert result["stdout"] == b"hello" + os.linesep.encode("utf-8") | |
19 | assert result["stderr"] == b"" | |
17 | assert result.returncode == 0 | |
18 | assert result.stdout == b"hello" + os.linesep.encode("utf-8") | |
19 | assert result.stderr == b"" | |
20 | 20 | |
21 | 21 | |
22 | 22 | def test_decode_invalid_utf8_input(capsys): |
27 | 27 | else: |
28 | 28 | command = ["cat"] |
29 | 29 | result = procrunner.run(command, stdin=test_string) |
30 | assert result["exitcode"] == 0 | |
31 | assert not result["stderr"] | |
30 | assert result.returncode == 0 | |
31 | assert not result.stderr | |
32 | 32 | if os.name == "nt": |
33 | 33 | # Windows modifies line endings |
34 | assert result["stdout"] == test_string[:-1] + b"\r\n" | |
34 | assert result.stdout == test_string[:-1] + b"\r\n" | |
35 | 35 | else: |
36 | assert result["stdout"] == test_string | |
36 | assert result.stdout == test_string | |
37 | 37 | out, err = capsys.readouterr() |
38 | 38 | assert out == u"test\ufffdstring\n" |
39 | 39 | assert err == u"" |
48 | 48 | if e.errno == 2: |
49 | 49 | pytest.skip("wget not available") |
50 | 50 | raise |
51 | assert result["exitcode"] == 0 | |
52 | assert b"http" in result["stderr"] | |
53 | assert b"google" in result["stdout"] | |
51 | assert result.returncode == 0 | |
52 | assert b"http" in result.stderr | |
53 | assert b"google" in result.stdout | |
54 | 54 | |
55 | 55 | |
56 | 56 | def test_path_object_resolution(tmpdir): |
57 | sentinel_value = "sentinel" | |
57 | sentinel_value = b"sentinel" | |
58 | 58 | tmpdir.join("tempfile").write(sentinel_value) |
59 | 59 | tmpdir.join("reader.py").write("print(open('tempfile').read())") |
60 | command = [sys.executable, tmpdir.join("reader.py")] | |
61 | 60 | result = procrunner.run( |
62 | command, | |
61 | [sys.executable, tmpdir.join("reader.py")], | |
63 | 62 | environment_override={"PYTHONHASHSEED": "random"}, |
64 | 63 | working_directory=tmpdir, |
65 | 64 | ) |
66 | assert result["exitcode"] == 0 | |
67 | assert not result["stderr"] | |
68 | assert sentinel_value == result["stdout"].strip().decode() | |
65 | assert result.returncode == 0 | |
66 | assert not result.stderr | |
67 | assert sentinel_value == result.stdout.strip() |