Reputation: 549
I implemented a Click CLI that will run subprocess
processes, but send their stdout
to stderr
, so that stdout
only contains the command's specific output, e.g.
@click.command()
def cli():
subprocess.run(["echo", "hello world"], stdout=sys.stderr)
click.echo("result")
And I want to test that "hello world"
goes to stderr
and "result"
goes to stdout
. Specifically, if I removed the stdout=sys.stderr
parameter, I want my test to fail.
def test_foo():
runner = CliRunner(mix_stderr=False)
result = runner.invoke(cli, catch_exceptions=False)
assert result.stdout == "result"
assert result.stderr == "hello world"
This doesn't work though, because it sets sys.stderr
to a handle without a file descriptor, which causes subprocess
to fail:
# Assuming file-like object
> c2pwrite = stdout.fileno()
E io.UnsupportedOperation: fileno
Is this a Click bug, or is there a workaround, or is this just not supported? I would like to avoid writing a full integration test that calls my CLI via subprocess
instead of CliRunner
.
Upvotes: 6
Views: 766
Reputation: 735
There is a closed issue in the click repo about this.
A possible workaround is to run your command in subprocess
with shell=True
and use shell redirection instead. For example
subprocess.run(["echo hello world >&2"], shell=True)
Upvotes: 1
Reputation: 38982
You can patch subprocess.run
and mock the stdout option so that it responds to the fileno
method.
In the POSIX standard, standard error stream file descriptor number is 2
. You can have your function return that.
You must then use the capfd
fixture to capture the standard error stream and perform your assertion.
import io
import subprocess
from unittest.mock import patch
from click.testing import CliRunner
from .. import main
def test_foo(capfd):
orig_run = subprocess.run
def patch_run(args, stdout=None):
assert stdout, 'You must configure stdout option'
assert isinstance(stdout, io.IOBase), 'stdout option must implemement stream interface'
assert stdout.name == '<stderr>', 'Set stdout option to stderr'
stdout.fileno = lambda: 2 # write to standard err fd
rv = orig_run(args, stdout=stdout)
return rv
with patch.object(subprocess, 'run', patch_run):
runner = CliRunner(mix_stderr=False)
result = runner.invoke(main.cli, catch_exceptions=False)
assert result.stdout == "result\n"
captured = capfd.readouterr()
assert captured.err == "hello world\n"
Upvotes: -1