def test_closes_stdin(self) -> None: with pytest.raises(InstallationSubprocessError): call_subprocess( [sys.executable, "-c", "input()"], show_stdout=True, command_desc="stdin reader", )
def install_editable( self, install_options, # type: List[str] global_options=(), # type: Sequence[str] prefix=None # type: Optional[str] ): # type: (...) -> None logger.info('Running setup.py develop for %s', self.name) if prefix: prefix_param = ['--prefix={}'.format(prefix)] install_options = list(install_options) + prefix_param base_cmd = make_setuptools_shim_args( self.setup_py_path, global_options=global_options, no_user_config=self.isolated ) with indent_log(): with self.build_env: call_subprocess( base_cmd + ['develop', '--no-deps'] + list(install_options), cwd=self.unpacked_source_directory, ) self.install_succeeded = True
def test_spinner_finish( self, exit_status: int, show_stdout: bool, extra_ok_returncodes: Optional[Tuple[int, ...]], log_level: int, caplog: pytest.LogCaptureFixture, expected: Tuple[Optional[Type[Exception]], Optional[str], int], ) -> None: """ Test that the spinner finishes correctly. """ expected_exc_type = expected[0] expected_final_status = expected[1] expected_spin_count = expected[2] command = f'print("Hello"); print("world"); exit({exit_status})' args, spinner = self.prepare_call(caplog, log_level, command=command) exc_type: Optional[Type[Exception]] try: call_subprocess( args, show_stdout=show_stdout, extra_ok_returncodes=extra_ok_returncodes, spinner=spinner, ) except Exception as exc: exc_type = type(exc) else: exc_type = None assert exc_type == expected_exc_type assert spinner.final_status == expected_final_status assert spinner.spin_count == expected_spin_count
def generate_metadata( build_env: BuildEnvironment, setup_py_path: str, source_dir: str, isolated: bool, details: str, ) -> str: """Generate metadata using setup.py-based defacto mechanisms. Returns the generated metadata directory. """ logger.debug( 'Running setup.py (path:%s) egg_info for package %s', setup_py_path, details, ) egg_info_dir = TempDirectory( kind="pip-egg-info", globally_managed=True ).path args = make_setuptools_egg_info_args( setup_py_path, egg_info_dir=egg_info_dir, no_user_config=isolated, ) with build_env: call_subprocess( args, cwd=source_dir, command_desc='python setup.py egg_info', ) # Return the .egg-info directory. return _find_egg_info(egg_info_dir)
def install_editable( install_options: List[str], global_options: Sequence[str], prefix: Optional[str], home: Optional[str], use_user_site: bool, name: str, setup_py_path: str, isolated: bool, build_env: BuildEnvironment, unpacked_source_directory: str, ) -> None: """Install a package in editable mode. Most arguments are pass-through to setuptools. """ logger.info("Running setup.py develop for %s", name) args = make_setuptools_develop_args( setup_py_path, global_options=global_options, install_options=install_options, no_user_config=isolated, prefix=prefix, home=home, use_user_site=use_user_site, ) with indent_log(): with build_env: call_subprocess( args, command_desc="python setup.py develop", cwd=unpacked_source_directory, )
def install_editable( self, install_options, # type: List[str] global_options=(), # type: Sequence[str] prefix=None # type: Optional[str] ): # type: (...) -> None logger.info('Running setup.py develop for %s', self.name) args = make_setuptools_develop_args( self.setup_py_path, global_options=global_options, install_options=install_options, no_user_config=self.isolated, prefix=prefix, ) with indent_log(): with self.build_env: call_subprocess( args, cwd=self.unpacked_source_directory, ) self.install_succeeded = True
def test_spinner_finish( self, exit_status, show_stdout, extra_ok_returncodes, log_level, caplog, expected, ): """ Test that the spinner finishes correctly. """ expected_exc_type = expected[0] expected_final_status = expected[1] expected_spin_count = expected[2] command = (f'print("Hello"); print("world"); exit({exit_status})') args, spinner = self.prepare_call(caplog, log_level, command=command) try: call_subprocess( args, show_stdout=show_stdout, extra_ok_returncodes=extra_ok_returncodes, spinner=spinner, ) except Exception as exc: exc_type = type(exc) else: exc_type = None assert exc_type == expected_exc_type assert spinner.final_status == expected_final_status assert spinner.spin_count == expected_spin_count
def install_editable( install_options, # type: List[str] global_options, # type: Sequence[str] prefix, # type: Optional[str] home, # type: Optional[str] use_user_site, # type: bool name, # type: str setup_py_path, # type: str isolated, # type: bool build_env, # type: BuildEnvironment unpacked_source_directory, # type: str ): # type: (...) -> None """Install a package in editable mode. Most arguments are pass-through to setuptools. """ logger.info('Running setup.py develop for %s', name) args = make_setuptools_develop_args( setup_py_path, global_options=global_options, install_options=install_options, no_user_config=isolated, prefix=prefix, home=home, use_user_site=use_user_site, ) with indent_log(): with build_env: call_subprocess( args, cwd=unpacked_source_directory, )
def test_info_logging__subprocess_error(self, capfd, caplog): """ Test INFO logging of a subprocess with an error (and without passing show_stdout=True). """ log_level = INFO command = 'print("Hello"); print("world"); exit("fail")' args, spinner = self.prepare_call(caplog, log_level, command=command) with pytest.raises(InstallationSubprocessError) as exc: call_subprocess(args, spinner=spinner) result = None exc_message = str(exc.value) assert exc_message.startswith( 'Command errored out with exit status 1: ') assert exc_message.endswith('Check the logs for full command output.') expected = (None, [ ('pip.subprocessor', ERROR, 'Complete output (3 lines):\n'), ]) # The spinner should spin three times in this case since the # subprocess output isn't being written to the console. self.check_result( capfd, caplog, log_level, spinner, result, expected, expected_spinner=(3, 'error'), ) # Do some further checking on the captured log records to confirm # that the subprocess output was logged. last_record = caplog.record_tuples[-1] last_message = last_record[2] lines = last_message.splitlines() # We have to sort before comparing the lines because we can't # guarantee the order in which stdout and stderr will appear. # For example, we observed the stderr lines coming before stdout # in CI for PyPy 2.7 even though stdout happens first chronologically. actual = sorted(lines) # Test the "command" line separately because we can't test an # exact match. command_line = actual.pop(1) assert actual == [ ' cwd: None', '----------------------------------------', 'Command errored out with exit status 1:', 'Complete output (3 lines):', 'Hello', 'fail', 'world', ], f'lines: {actual}' # Show the full output on failure. assert command_line.startswith(' command: ') assert command_line.endswith('print("world"); exit("fail")\'')
def _install_requirements( pip_runnable: str, finder: "PackageFinder", requirements: Iterable[str], prefix: _Prefix, *, kind: str, ) -> None: args: List[str] = [ sys.executable, pip_runnable, "install", "--ignore-installed", "--no-user", "--prefix", prefix.path, "--no-warn-script-location", ] if logger.getEffectiveLevel() <= logging.DEBUG: args.append("-v") for format_control in ("no_binary", "only_binary"): formats = getattr(finder.format_control, format_control) args.extend( ( "--" + format_control.replace("_", "-"), ",".join(sorted(formats or {":none:"})), ) ) index_urls = finder.index_urls if index_urls: args.extend(["-i", index_urls[0]]) for extra_index in index_urls[1:]: args.extend(["--extra-index-url", extra_index]) else: args.append("--no-index") for link in finder.find_links: args.extend(["--find-links", link]) for host in finder.trusted_hosts: args.extend(["--trusted-host", host]) if finder.allow_all_prereleases: args.append("--pre") if finder.prefer_binary: args.append("--prefer-binary") args.append("--") args.extend(requirements) extra_environ = {"_PIP_STANDALONE_CERT": where()} with open_spinner(f"Installing {kind}") as spinner: call_subprocess( args, command_desc=f"pip subprocess to install {kind}", spinner=spinner, extra_environ=extra_environ, )
def _clean_one(self, req): base_args = self._base_setup_args(req) logger.info('Running setup.py clean for %s', req.name) clean_args = base_args + ['clean', '--all'] try: call_subprocess(clean_args, cwd=req.source_dir) return True except Exception: logger.error('Failed cleaning build dir for %s', req.name) return False
def _clean_one_legacy(req, global_options): # type: (InstallRequirement, List[str]) -> bool clean_args = make_setuptools_clean_args(req.setup_py_path, global_options=global_options) logger.info("Running setup.py clean for %s", req.name) try: call_subprocess(clean_args, cwd=req.source_dir) return True except Exception: logger.error("Failed cleaning build dir for %s", req.name) return False
def install_requirements( self, finder, # type: PackageFinder requirements, # type: Iterable[str] prefix_as_string, # type: str message, # type: str ): # type: (...) -> None prefix = self._prefixes[prefix_as_string] assert not prefix.setup prefix.setup = True if not requirements: return args = [ sys.executable, os.path.dirname(pip_location), "install", "--ignore-installed", "--no-user", "--prefix", prefix.path, "--no-warn-script-location", ] # type: List[str] if logger.getEffectiveLevel() <= logging.DEBUG: args.append("-v") for format_control in ("no_binary", "only_binary"): formats = getattr(finder.format_control, format_control) args.extend(( "--" + format_control.replace("_", "-"), ",".join(sorted(formats or {":none:"})), )) index_urls = finder.index_urls if index_urls: args.extend(["-i", index_urls[0]]) for extra_index in index_urls[1:]: args.extend(["--extra-index-url", extra_index]) else: args.append("--no-index") for link in finder.find_links: args.extend(["--find-links", link]) for host in finder.trusted_hosts: args.extend(["--trusted-host", host]) if finder.allow_all_prereleases: args.append("--pre") if finder.prefer_binary: args.append("--prefer-binary") args.append("--") args.extend(requirements) with open_spinner(message) as spinner: call_subprocess(args, spinner=spinner)
def _clean_one(self, req): clean_args = make_setuptools_clean_args( req.setup_py_path, global_options=self.global_options, ) logger.info('Running setup.py clean for %s', req.name) try: call_subprocess(clean_args, cwd=req.source_dir) return True except Exception: logger.error('Failed cleaning build dir for %s', req.name) return False
def install_requirements( self, finder, # type: PackageFinder requirements, # type: Iterable[str] prefix_as_string, # type: str message # type: str ): # type: (...) -> None prefix = self._prefixes[prefix_as_string] assert not prefix.setup prefix.setup = True if not requirements: return args = [ sys.executable, os.path.dirname(pip_location), 'install', '--ignore-installed', '--no-user', '--prefix', prefix.path, '--no-warn-script-location', ] # type: List[str] if logger.getEffectiveLevel() <= logging.DEBUG: args.append('-v') for format_control in ('no_binary', 'only_binary'): formats = getattr(finder.format_control, format_control) args.extend(('--' + format_control.replace('_', '-'), ','.join(sorted(formats or {':none:'})))) index_urls = finder.index_urls if index_urls: args.extend(['-i', index_urls[0]]) for extra_index in index_urls[1:]: args.extend(['--extra-index-url', extra_index]) else: args.append('--no-index') for link in finder.find_links: args.extend(['--find-links', link]) for host in finder.trusted_hosts: args.extend(['--trusted-host', host]) if finder.allow_all_prereleases: args.append('--pre') if finder.prefer_binary: args.append('--prefer-binary') args.append('--') args.extend(requirements) with open_spinner(message) as spinner: call_subprocess(args, spinner=spinner)
def test_unicode_decode_error(caplog): if locale.getpreferredencoding() != "UTF-8": pytest.skip("locale.getpreferredencoding() is not UTF-8") caplog.set_level(INFO) call_subprocess([ sys.executable, "-c", "import sys; sys.stdout.buffer.write(b'\\xff')", ], show_stdout=True) assert len(caplog.records) == 2 # First log record is "Running command ..." assert caplog.record_tuples[1] == ("pip.subprocessor", INFO, "\\xff")
def test_info_logging__subprocess_error( self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture) -> None: """ Test INFO logging of a subprocess with an error (and without passing show_stdout=True). """ log_level = INFO command = 'print("Hello"); print("world"); exit("fail")' args, spinner = self.prepare_call(caplog, log_level, command=command) with pytest.raises(InstallationSubprocessError) as exc: call_subprocess( args, command_desc="test info logging with subprocess error", spinner=spinner, ) result = None exception = exc.value assert exception.reference == "subprocess-exited-with-error" assert "exit code: 1" in exception.message assert exception.note_stmt assert "not a problem with pip" in exception.note_stmt # Check that the process output is captured, and would be shown. assert exception.context assert "Hello\n" in exception.context assert "fail\n" in exception.context assert "world\n" in exception.context expected = ( None, [ # pytest's caplog overrides th formatter, which means that we # won't see the message formatted through our formatters. ("pip.subprocessor", ERROR, "[present-diagnostic]"), ], ) # The spinner should spin three times in this case since the # subprocess output isn't being written to the console. self.check_result( capfd, caplog, log_level, spinner, result, expected, expected_spinner=(3, "error"), )
def test_info_logging(self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture) -> None: """ Test INFO logging (and without passing show_stdout=True). """ log_level = INFO args, spinner = self.prepare_call(caplog, log_level) result = call_subprocess( args, command_desc="test info logging", spinner=spinner, ) expected: Tuple[List[str], List[Tuple[str, int, str]]] = ( ["Hello", "world"], [], ) # The spinner should spin twice in this case since the subprocess # output isn't being written to the console. self.check_result( capfd, caplog, log_level, spinner, result, expected, expected_spinner=(2, "done"), )
def test_info_logging_with_show_stdout_true( self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture) -> None: """ Test INFO logging with show_stdout=True. """ log_level = INFO args, spinner = self.prepare_call(caplog, log_level) result = call_subprocess(args, spinner=spinner, show_stdout=True) expected = ( ["Hello", "world"], [ ("pip.subprocessor", INFO, "Running command "), ("pip.subprocessor", INFO, "Hello"), ("pip.subprocessor", INFO, "world"), ], ) # The spinner shouldn't spin in this case since the subprocess # output is already being written to the console. self.check_result( capfd, caplog, log_level, spinner, result, expected, expected_spinner=(0, None), )
def test_debug_logging(self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture) -> None: """ Test DEBUG logging (and without passing show_stdout=True). """ log_level = DEBUG args, spinner = self.prepare_call(caplog, log_level) result = call_subprocess(args, spinner=spinner) expected = ( ["Hello", "world"], [ ("pip.subprocessor", VERBOSE, "Running command "), ("pip.subprocessor", VERBOSE, "Hello"), ("pip.subprocessor", VERBOSE, "world"), ], ) # The spinner shouldn't spin in this case since the subprocess # output is already being logged to the console. self.check_result( capfd, caplog, log_level, spinner, result, expected, expected_spinner=(0, None), )
def test_call_subprocess_stdout_only( capfd: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, stdout_only: bool, expected: Tuple[str, ...], ) -> None: log = [] monkeypatch.setattr( subprocess_logger, "log", lambda level, *args: log.append(args[0]), ) out = call_subprocess( [ sys.executable, "-c", "import sys; sys.stdout.write('out\\n'); sys.stderr.write('err\\n')", ], stdout_only=stdout_only, ) assert out in expected captured = capfd.readouterr() assert captured.err == "" assert log == ["Running command %s", "out", "err"] or log == [ "Running command %s", "err", "out", ]
def _build_one_legacy(self, req, tempd, python_tag=None): """Build one InstallRequirement using the "legacy" build process. Returns path to wheel if successfully built. Otherwise, returns None. """ base_args = self._base_setup_args(req) spin_message = "Building wheel for %s (setup.py)" % (req.name,) with open_spinner(spin_message) as spinner: logger.debug("Destination directory: %s", tempd) wheel_args = base_args + ["bdist_wheel", "-d", tempd] + self.build_options if python_tag is not None: wheel_args += ["--python-tag", python_tag] try: output = call_subprocess( wheel_args, cwd=req.unpacked_source_directory, spinner=spinner ) except Exception: spinner.finish("error") logger.error("Failed building wheel for %s", req.name) return None names = os.listdir(tempd) wheel_path = get_legacy_build_wheel_path( names=names, temp_dir=tempd, req=req, command_args=wheel_args, command_output=output, ) return wheel_path
def generate_metadata(install_req): # type: (InstallRequirement) -> str """Generate metadata using setup.py-based defacto mechanisms.ArithmeticError Returns the generated metadata directory. """ assert install_req.unpacked_source_directory req_details_str = install_req.name or "from {}".format(install_req.link) logger.debug( 'Running setup.py (path:%s) egg_info for package %s', install_req.setup_py_path, req_details_str, ) egg_info_dir = None # type: Optional[str] # For non-editable installs, don't put the .egg-info files at the root, # to avoid confusion due to the source code being considered an installed # egg. if not install_req.editable: egg_info_dir = os.path.join( install_req.unpacked_source_directory, 'pip-egg-info', ) # setuptools complains if the target directory does not exist. ensure_dir(egg_info_dir) args = make_setuptools_egg_info_args( install_req.setup_py_path, egg_info_dir=egg_info_dir, no_user_config=install_req.isolated, ) with install_req.build_env: call_subprocess( args, cwd=install_req.unpacked_source_directory, command_desc='python setup.py egg_info', ) # Return the .egg-info directory. return _find_egg_info( install_req.unpacked_source_directory, install_req.editable, )
def _install_requirements( standalone_pip: str, finder: "PackageFinder", requirements: Iterable[str], prefix: _Prefix, message: str, ) -> None: args = [ sys.executable, standalone_pip, 'install', '--ignore-installed', '--no-user', '--prefix', prefix.path, '--no-warn-script-location', ] # type: List[str] if logger.getEffectiveLevel() <= logging.DEBUG: args.append('-v') for format_control in ('no_binary', 'only_binary'): formats = getattr(finder.format_control, format_control) args.extend(('--' + format_control.replace('_', '-'), ','.join(sorted(formats or {':none:'})))) index_urls = finder.index_urls if index_urls: args.extend(['-i', index_urls[0]]) for extra_index in index_urls[1:]: args.extend(['--extra-index-url', extra_index]) else: args.append('--no-index') for link in finder.find_links: args.extend(['--find-links', link]) for host in finder.trusted_hosts: args.extend(['--trusted-host', host]) if finder.allow_all_prereleases: args.append('--pre') if finder.prefer_binary: args.append('--prefer-binary') args.append('--') args.extend(requirements) extra_environ = {"_PIP_STANDALONE_CERT": where()} with open_spinner(message) as spinner: call_subprocess(args, spinner=spinner, extra_environ=extra_environ)
def generate_metadata( build_env, # type: BuildEnvironment setup_py_path, # type: str source_dir, # type: str editable, # type: bool isolated, # type: bool details, # type: str ): # type: (...) -> str """Generate metadata using setup.py-based defacto mechanisms. Returns the generated metadata directory. """ logger.debug( 'Running setup.py (path:%s) egg_info for package %s', setup_py_path, details, ) egg_info_dir = None # type: Optional[str] # For non-editable installs, don't put the .egg-info files at the root, # to avoid confusion due to the source code being considered an installed # egg. if not editable: egg_info_dir = os.path.join(source_dir, 'pip-egg-info') # setuptools complains if the target directory does not exist. ensure_dir(egg_info_dir) args = make_setuptools_egg_info_args( setup_py_path, egg_info_dir=egg_info_dir, no_user_config=isolated, ) with build_env: call_subprocess( args, cwd=source_dir, command_desc='python setup.py egg_info', ) # Return the .egg-info directory. return _find_egg_info(source_dir, editable)
def generate_metadata( build_env: BuildEnvironment, setup_py_path: str, source_dir: str, isolated: bool, details: str, ) -> str: """Generate metadata using setup.py-based defacto mechanisms. Returns the generated metadata directory. """ logger.debug( "Running setup.py (path:%s) egg_info for package %s", setup_py_path, details, ) egg_info_dir = TempDirectory(kind="pip-egg-info", globally_managed=True).path args = make_setuptools_egg_info_args( setup_py_path, egg_info_dir=egg_info_dir, no_user_config=isolated, ) with build_env: with open_spinner("Preparing metadata (setup.py)") as spinner: try: call_subprocess( args, cwd=source_dir, command_desc="python setup.py egg_info", spinner=spinner, ) except InstallationSubprocessError as error: raise MetadataGenerationFailed( package_details=details) from error # Return the .egg-info directory. return _find_egg_info(egg_info_dir)
def run_command( cls, cmd, # type: Union[List[str], CommandArgs] show_stdout=True, # type: bool cwd=None, # type: Optional[str] on_returncode="raise", # type: Literal["raise", "warn", "ignore"] extra_ok_returncodes=None, # type: Optional[Iterable[int]] command_desc=None, # type: Optional[str] extra_environ=None, # type: Optional[Mapping[str, Any]] spinner=None, # type: Optional[SpinnerInterface] log_failed_cmd=True, # type: bool stdout_only=False, # type: bool ): # type: (...) -> str """ Run a VCS subcommand This is simply a wrapper around call_subprocess that adds the VCS command name, and checks that the VCS is available """ cmd = make_command(cls.name, *cmd) try: return call_subprocess( cmd, show_stdout, cwd, on_returncode=on_returncode, extra_ok_returncodes=extra_ok_returncodes, command_desc=command_desc, extra_environ=extra_environ, unset_environ=cls.unset_environ, spinner=spinner, log_failed_cmd=log_failed_cmd, stdout_only=stdout_only, ) except FileNotFoundError: # errno.ENOENT = no such file or directory # In other words, the VCS executable isn't available raise BadCommand( f"Cannot find command {cls.name!r} - do you have " f"{cls.name!r} installed and in your PATH?" ) except PermissionError: # errno.EACCES = Permission denied # This error occurs, for instance, when the command is installed # only for another user. So, the current user don't have # permission to call the other user command. raise BadCommand( f"No permission to execute {cls.name!r} - install it " f"locally, globally (ask admin), or check your PATH. " f"See possible solutions at " f"https://pip.pypa.io/en/latest/reference/pip_freeze/" f"#fixing-permission-denied." )
def _generate_metadata_legacy(install_req): # type: (InstallRequirement) -> str req_details_str = install_req.name or "from {}".format(install_req.link) logger.debug( 'Running setup.py (path:%s) egg_info for package %s', install_req.setup_py_path, req_details_str, ) # Compose arguments for subprocess call base_cmd = make_setuptools_shim_args(install_req.setup_py_path) if install_req.isolated: base_cmd += ["--no-user-cfg"] # For non-editable installs, don't put the .egg-info files at the root, # to avoid confusion due to the source code being considered an installed # egg. egg_base_option = [] # type: List[str] if not install_req.editable: egg_info_dir = os.path.join( install_req.unpacked_source_directory, 'pip-egg-info', ) egg_base_option = ['--egg-base', egg_info_dir] # setuptools complains if the target directory does not exist. ensure_dir(egg_info_dir) with install_req.build_env: call_subprocess( base_cmd + ["egg_info"] + egg_base_option, cwd=install_req.unpacked_source_directory, command_desc='python setup.py egg_info', ) # Return the .egg-info directory. return _find_egg_info( install_req.unpacked_source_directory, install_req.editable, )
def run_command( cls, cmd, # type: Union[List[str], CommandArgs] show_stdout=True, # type: bool cwd=None, # type: Optional[str] on_returncode='raise', # type: str extra_ok_returncodes=None, # type: Optional[Iterable[int]] command_desc=None, # type: Optional[str] extra_environ=None, # type: Optional[Mapping[str, Any]] spinner=None, # type: Optional[SpinnerInterface] log_failed_cmd=True, # type: bool stdout_only=False, # type: bool ): # type: (...) -> str """ Run a VCS subcommand This is simply a wrapper around call_subprocess that adds the VCS command name, and checks that the VCS is available """ cmd = make_command(cls.name, *cmd) try: return call_subprocess(cmd, show_stdout, cwd, on_returncode=on_returncode, extra_ok_returncodes=extra_ok_returncodes, command_desc=command_desc, extra_environ=extra_environ, unset_environ=cls.unset_environ, spinner=spinner, log_failed_cmd=log_failed_cmd, stdout_only=stdout_only) except FileNotFoundError: # errno.ENOENT = no such file or directory # In other words, the VCS executable isn't available raise BadCommand('Cannot find command {cls.name!r} - do you have ' '{cls.name!r} installed and in your ' 'PATH?'.format(**locals()))
def test_info_logging(self, capfd, caplog): """ Test INFO logging (and without passing show_stdout=True). """ log_level = INFO args, spinner = self.prepare_call(caplog, log_level) result = call_subprocess(args, spinner=spinner) expected = (['Hello', 'world'], []) # The spinner should spin twice in this case since the subprocess # output isn't being written to the console. self.check_result( capfd, caplog, log_level, spinner, result, expected, expected_spinner=(2, 'done'), )