|
6 | 6 | """
|
7 | 7 | import codecs
|
8 | 8 | import itertools
|
| 9 | +import logging |
9 | 10 | import os
|
10 | 11 | import shutil
|
11 | 12 | import stat
|
|
33 | 34 | )
|
34 | 35 | from pip._internal.utils.packaging import check_dist_requires_python
|
35 | 36 | from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
|
| 37 | +from pip._internal.utils.ui import SpinnerInterface |
36 | 38 |
|
37 | 39 |
|
38 | 40 | class Tests_EgglinkPath:
|
@@ -750,28 +752,236 @@ def test_format_command_args(args, expected):
|
750 | 752 | assert actual == expected
|
751 | 753 |
|
752 | 754 |
|
753 |
| -def test_call_subprocess_works__no_keyword_arguments(): |
754 |
| - result = call_subprocess( |
755 |
| - [sys.executable, '-c', 'print("Hello")'], |
756 |
| - ) |
757 |
| - assert result.rstrip() == 'Hello' |
| 755 | +class FakeSpinner(SpinnerInterface): |
758 | 756 |
|
| 757 | + def __init__(self): |
| 758 | + self.spin_count = 0 |
| 759 | + self.final_status = None |
759 | 760 |
|
760 |
| -def test_call_subprocess_works__show_stdout_true(): |
761 |
| - result = call_subprocess( |
762 |
| - [sys.executable, '-c', 'print("Hello")'], |
763 |
| - show_stdout=True, |
764 |
| - ) |
765 |
| - assert result is None |
| 761 | + def spin(self): |
| 762 | + self.spin_count += 1 |
| 763 | + |
| 764 | + def finish(self, final_status): |
| 765 | + self.final_status = final_status |
| 766 | + |
| 767 | + |
| 768 | +class TestCallSubprocess(object): |
| 769 | + |
| 770 | + """ |
| 771 | + Test call_subprocess(). |
| 772 | + """ |
| 773 | + |
| 774 | + def check_result( |
| 775 | + self, capfd, caplog, log_level, spinner, result, expected, |
| 776 | + expected_spinner, |
| 777 | + ): |
| 778 | + """ |
| 779 | + Check the result of calling call_subprocess(). |
| 780 | +
|
| 781 | + :param log_level: the logging level that caplog was set to. |
| 782 | + :param spinner: the FakeSpinner object passed to call_subprocess() |
| 783 | + to be checked. |
| 784 | + :param result: the call_subprocess() return value to be checked. |
| 785 | + :param expected: a 3-tuple (expected_proc, expected_out, |
| 786 | + expected_records), where |
| 787 | + 1) `expected_proc` is the expected return value of |
| 788 | + call_subprocess() as a list of lines, or None if the return |
| 789 | + value is expected to be None; |
| 790 | + 2) `expected_out` is the expected stdout captured from the |
| 791 | + subprocess call, as a list of lines; and |
| 792 | + 3) `expected_records` is the expected value of |
| 793 | + caplog.record_tuples. |
| 794 | + :param expected_spinner: a 2-tuple of the spinner's expected |
| 795 | + (spin_count, final_status). |
| 796 | + """ |
| 797 | + expected_proc, expected_out, expected_records = expected |
| 798 | + |
| 799 | + if expected_proc is None: |
| 800 | + assert result is expected_proc |
| 801 | + else: |
| 802 | + assert result.splitlines() == expected_proc |
| 803 | + |
| 804 | + captured = capfd.readouterr() |
| 805 | + stdout, stderr = captured.out, captured.err |
| 806 | + |
| 807 | + assert stdout.splitlines() == expected_out |
| 808 | + assert stderr == '' |
| 809 | + |
| 810 | + records = caplog.record_tuples |
| 811 | + if len(records) != len(expected_records): |
| 812 | + raise RuntimeError('{} != {}'.format(records, expected_records)) |
| 813 | + |
| 814 | + for record, expected_record in zip(records, expected_records): |
| 815 | + # Check the logger_name and log level parts exactly. |
| 816 | + assert record[:2] == expected_record[:2] |
| 817 | + # For the message portion, check only a substring. Also, we |
| 818 | + # can't use startswith() since the order of stdout and stderr |
| 819 | + # isn't guaranteed in cases where stderr is also present. |
| 820 | + # For example, we observed the stderr lines coming before stdout |
| 821 | + # in CI for PyPy 2.7 even though stdout happens first |
| 822 | + # chronologically. |
| 823 | + assert expected_record[2] in record[2] |
| 824 | + |
| 825 | + assert (spinner.spin_count, spinner.final_status) == expected_spinner |
| 826 | + |
| 827 | + def prepare_call(self, caplog, log_level, command=None): |
| 828 | + if command is None: |
| 829 | + command = 'print("Hello"); print("world")' |
| 830 | + |
| 831 | + caplog.set_level(log_level) |
| 832 | + spinner = FakeSpinner() |
| 833 | + args = [sys.executable, '-c', command] |
| 834 | + |
| 835 | + return (args, spinner) |
| 836 | + |
| 837 | + def test_debug_logging(self, capfd, caplog): |
| 838 | + """ |
| 839 | + Test DEBUG logging (and without passing show_stdout=True). |
| 840 | + """ |
| 841 | + log_level = logging.DEBUG |
| 842 | + args, spinner = self.prepare_call(caplog, log_level) |
| 843 | + result = call_subprocess(args, spinner=spinner) |
| 844 | + |
| 845 | + expected = (['Hello', 'world'], [], [ |
| 846 | + ('pip._internal.utils.misc', 10, 'Running command '), |
| 847 | + ('pip._internal.utils.misc', 10, 'Hello'), |
| 848 | + ('pip._internal.utils.misc', 10, 'world'), |
| 849 | + ]) |
| 850 | + # The spinner shouldn't spin in this case since the subprocess |
| 851 | + # output is already being logged to the console. |
| 852 | + self.check_result( |
| 853 | + capfd, caplog, log_level, spinner, result, expected, |
| 854 | + expected_spinner=(0, 'done'), |
| 855 | + ) |
766 | 856 |
|
| 857 | + def test_info_logging(self, capfd, caplog): |
| 858 | + """ |
| 859 | + Test INFO logging (and without passing show_stdout=True). |
| 860 | + """ |
| 861 | + log_level = logging.INFO |
| 862 | + args, spinner = self.prepare_call(caplog, log_level) |
| 863 | + result = call_subprocess(args, spinner=spinner) |
| 864 | + |
| 865 | + expected = (['Hello', 'world'], [], []) |
| 866 | + # The spinner should spin twice in this case since the subprocess |
| 867 | + # output isn't being written to the console. |
| 868 | + self.check_result( |
| 869 | + capfd, caplog, log_level, spinner, result, expected, |
| 870 | + expected_spinner=(2, 'done'), |
| 871 | + ) |
767 | 872 |
|
768 |
| -def test_call_subprocess_closes_stdin(): |
769 |
| - with pytest.raises(InstallationError): |
770 |
| - call_subprocess( |
771 |
| - [sys.executable, '-c', 'input()'], |
772 |
| - show_stdout=True, |
| 873 | + def test_info_logging__subprocess_error(self, capfd, caplog): |
| 874 | + """ |
| 875 | + Test INFO logging of a subprocess with an error (and without passing |
| 876 | + show_stdout=True). |
| 877 | + """ |
| 878 | + log_level = logging.INFO |
| 879 | + command = 'print("Hello"); print("world"); exit("fail")' |
| 880 | + args, spinner = self.prepare_call(caplog, log_level, command=command) |
| 881 | + |
| 882 | + with pytest.raises(InstallationError): |
| 883 | + call_subprocess(args, spinner=spinner) |
| 884 | + result = None |
| 885 | + |
| 886 | + expected = (None, [], [ |
| 887 | + ('pip._internal.utils.misc', 20, 'Complete output from command '), |
| 888 | + # The "failed" portion is later on in this "Hello" string. |
| 889 | + ('pip._internal.utils.misc', 20, 'Hello'), |
| 890 | + ]) |
| 891 | + # The spinner should spin three times in this case since the |
| 892 | + # subprocess output isn't being written to the console. |
| 893 | + self.check_result( |
| 894 | + capfd, caplog, log_level, spinner, result, expected, |
| 895 | + expected_spinner=(3, 'error'), |
773 | 896 | )
|
774 | 897 |
|
| 898 | + # Do some further checking on the captured log records to confirm |
| 899 | + # that the subprocess output was logged. |
| 900 | + last_record = caplog.record_tuples[-1] |
| 901 | + last_message = last_record[2] |
| 902 | + lines = last_message.splitlines() |
| 903 | + |
| 904 | + # We have to sort before comparing the lines because we can't |
| 905 | + # guarantee the order in which stdout and stderr will appear. |
| 906 | + # For example, we observed the stderr lines coming before stdout |
| 907 | + # in CI for PyPy 2.7 even though stdout happens first chronologically. |
| 908 | + assert sorted(lines) == [ |
| 909 | + '----------------------------------------', |
| 910 | + 'Hello', |
| 911 | + 'fail', |
| 912 | + 'world', |
| 913 | + ], 'lines: {}'.format(lines) # Show the full output on failure. |
| 914 | + |
| 915 | + def test_info_logging_with_show_stdout_true(self, capfd, caplog): |
| 916 | + """ |
| 917 | + Test INFO logging with show_stdout=True. |
| 918 | + """ |
| 919 | + log_level = logging.INFO |
| 920 | + args, spinner = self.prepare_call(caplog, log_level) |
| 921 | + result = call_subprocess(args, spinner=spinner, show_stdout=True) |
| 922 | + |
| 923 | + expected = (None, ['Hello', 'world'], []) |
| 924 | + # The spinner shouldn't spin in this case since the subprocess |
| 925 | + # output is already being written to the console. |
| 926 | + self.check_result( |
| 927 | + capfd, caplog, log_level, spinner, result, expected, |
| 928 | + expected_spinner=(0, None), |
| 929 | + ) |
| 930 | + |
| 931 | + @pytest.mark.parametrize(( |
| 932 | + 'exit_status', 'show_stdout', 'extra_ok_returncodes', 'log_level', |
| 933 | + 'expected'), |
| 934 | + [ |
| 935 | + (0, False, None, logging.INFO, (None, 'done', 2)), |
| 936 | + # Test some cases that should result in show_spinner false. |
| 937 | + (0, False, None, logging.DEBUG, (None, 'done', 0)), |
| 938 | + # Test show_stdout=True. |
| 939 | + (0, True, None, logging.DEBUG, (None, None, 0)), |
| 940 | + (0, True, None, logging.INFO, (None, None, 0)), |
| 941 | + (0, True, None, logging.WARNING, (None, None, 0)), |
| 942 | + # Test a non-zero exit status. |
| 943 | + (3, False, None, logging.INFO, (InstallationError, 'error', 2)), |
| 944 | + # Test a non-zero exit status also in extra_ok_returncodes. |
| 945 | + (3, False, (3, ), logging.INFO, (None, 'done', 2)), |
| 946 | + ]) |
| 947 | + def test_spinner_finish( |
| 948 | + self, exit_status, show_stdout, extra_ok_returncodes, log_level, |
| 949 | + caplog, expected, |
| 950 | + ): |
| 951 | + """ |
| 952 | + Test that the spinner finishes correctly. |
| 953 | + """ |
| 954 | + expected_exc_type = expected[0] |
| 955 | + expected_final_status = expected[1] |
| 956 | + expected_spin_count = expected[2] |
| 957 | + |
| 958 | + command = ( |
| 959 | + 'print("Hello"); print("world"); exit({})'.format(exit_status) |
| 960 | + ) |
| 961 | + args, spinner = self.prepare_call(caplog, log_level, command=command) |
| 962 | + try: |
| 963 | + call_subprocess( |
| 964 | + args, |
| 965 | + show_stdout=show_stdout, |
| 966 | + extra_ok_returncodes=extra_ok_returncodes, |
| 967 | + spinner=spinner, |
| 968 | + ) |
| 969 | + except Exception as exc: |
| 970 | + exc_type = type(exc) |
| 971 | + else: |
| 972 | + exc_type = None |
| 973 | + |
| 974 | + assert exc_type == expected_exc_type |
| 975 | + assert spinner.final_status == expected_final_status |
| 976 | + assert spinner.spin_count == expected_spin_count |
| 977 | + |
| 978 | + def test_closes_stdin(self): |
| 979 | + with pytest.raises(InstallationError): |
| 980 | + call_subprocess( |
| 981 | + [sys.executable, '-c', 'input()'], |
| 982 | + show_stdout=True, |
| 983 | + ) |
| 984 | + |
775 | 985 |
|
776 | 986 | @pytest.mark.parametrize('args, expected', [
|
777 | 987 | # Test without subdir.
|
|
0 commit comments