Running a subprocess fails when not using pytest no-capture

Viewed 387

Setup

I've simplified the problem to the core:

  1. Having pytest installed
pip install pytest==5.4.3
  1. Having a shell script
# has-stdin.sh

# Detect stdin
if [[ ! -t 0 ]]; then
    echo "yes"
else
    echo "no"
fi
  1. Having a Python test
# test.py

import subprocess
import unittest


class TestCase(unittest.TestCase):

    def test(self):
        process = subprocess.run(
            ["sh", "./has-stdin.sh"],
            stderr=subprocess.PIPE,
            stdout=subprocess.PIPE,
            check=True,
            shell=False
        )
        assert process.stdout.decode("utf-8") == "no\n"


Testing

✅ Script works in a bash shell

$ sh ./has-stdin.sh
no
$ echo '' | sh ./has-stdin.sh
yes
$ sh ./has-stdin.sh <<< ''
yes

✅ Runs successfully with -s (e.g. --capture=no)

$ pytest test.py -s
platform linux -- Python 3.7.7, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /Users/maikel/docker/library/postgresql
collected 1 item

test.py .

=========================== 1 passed in 0.02s ===========================

❌ Runs unsuccessfully without -s

$ pytest test.py
========================== test session starts ==========================
platform linux -- Python 3.7.7, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /Users/maikel/docker/library/postgresql
collected 1 item

test.py F                                                                                                                                                                                                                      [100%]

================================ FAILURES ===============================
_____________________________ TestCase.test _____________________________

self = <test.TestCase testMethod=test>

    def test(self):
        process = subprocess.run(
            ["sh", "./has-stdin.sh"],
            stderr=subprocess.PIPE,
            stdout=subprocess.PIPE,
            check=True,
            shell=False
        )
>       assert process.stdout.decode("utf-8") == "no\n"
E       AssertionError: assert 'yes\n' == 'no\n'
E         - no
E         + yes

test.py:23: AssertionError
======================== short test summary info ========================
FAILED test.py::TestCase::test - AssertionError: assert 'yes\n' == 'no\n'
=========================== 1 failed in 0.10s ===========================


What is making the difference in using -s? How to run this pytest successfully without -s?

I've tried Test calling of an external script run via Popen that expects no available data in stdin using pytest, but when using that method both pytest commands fail.

2 Answers

That's a very interesting problem.

I got so intrigued so I did run the code locally and examined what's happening:

In the first case (running without -s) the shell's /proc/self/fd/0 is /dev/null but running with -s the shell's /proc/self/fd/0 is /dev/pts/7 in my case.

$ pgrep -f has-st
12727
$ ls -la  /proc/12727/fd/0 
lrwx------ 1 quoyn quoyn 64 Aug  4 00:13 /proc/12727/fd/0 -> /dev/pts/7

and

$ pgrep -f has-st
12793
$ ls -la  /proc/12793/fd/0 
lr-x------ 1 quoyn quoyn 64 Aug  4 00:17 /proc/12793/fd/0 -> /dev/null

This si because when you run with "-s" pytest does not capture the terminal and simply passes it through, that's why it is /dev/pts/7. If I run ls -l /proc/self/fd/0 in my terminal after pytest has quit I indeed get:

lrwx------ 1 quoyn quoyn 64 Aug  4 00:29 /proc/self/fd/0 -> /dev/pts/7

You can try without pytest as well, only python:

import subprocess
s = subprocess.run(["ls", "-l", "/proc/self/fd/0"], stderr=subprocess.PIPE, stdout=subprocess.PIPE, input=None, check=True, shell=False)
print(s.stdout)

And try executing as python code_above.py and echo '' | python code_above.py

EDIT:

And to answer the second question

How to run this pytest successfully without -s?

Use a pseudo terminal yourself. Run the subprocess in a pty.\

EDIT2:

The mimimal test to reproduce/demonstrate this problem, run the following code as pytest test.py and pytest -s test.py:

import sys
def test():
    assert sys.stdin.isatty()
$ pip install ptyprocess==0.6.0
# test.py

import unittest

from ptyprocess import PtyProcess


class TestCase(unittest.TestCase):

    def test(self):
        process = PtyProcess.spawn(
            ["sh", "./has-stdin.sh"]
        )
        process.wait()
        output = process.read().decode("utf-8")
        if process.exitstatus:
            raise Exception(output.strip("\r\n"))
        assert output.strip("\r\n") == 'no'

✅ Runs successfully with -s:

$ pytest test.py -s
platform linux -- Python 3.7.7, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /Users/maikel/docker/library/postgresql
collected 1 item

test.py .

=========================== 1 passed in 0.60s ===========================

✅ Runs successfully without -s:

$ pytest test.py
platform linux -- Python 3.7.7, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
rootdir: /Users/maikel/docker/library/postgresql
collected 1 item

test.py .

=========================== 1 passed in 0.60s ===========================

Caveats

  • It runs quite a bit slower than subprocess (0.60s vs 0.02s in this example)
  • There is no distinction between stdout and stderr

The situation is explained by this diagram:

pty-diagram

Source: https://github.com/pexpect/ptyprocess/blob/3931cd4/docs/index.rst

Related