brandonchinn178
brandonchinn178

Reputation: 549

How do I test that command sends subprocess output to stderr?

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

Answers (2)

Samuel Rivas
Samuel Rivas

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

Oluwafemi Sule
Oluwafemi Sule

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

Related Questions