pexpect/ptyprocess

test failure on FreeBSD

emaste opened this issue · 5 comments

Running the tests on FreeBSD 11 I see:

% py.test  
============================= test session starts ==============================
platform freebsd11 -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4
collected 5 items 

tests/test_invalid_binary.py .
tests/test_preexec_fn.py ..
tests/test_spawn.py FF


=================================== FAILURES ===================================
__________________________ PtyTestCase.test_spawn_sh ___________________________

self = <tests.test_spawn.PtyTestCase testMethod=test_spawn_sh>

    def test_spawn_sh(self):
        env = os.environ.copy()
        env['FOO'] = 'rebar'
        p = PtyProcess.spawn(['sh'], env=env)
        p.read()
        p.write(b'echo $FOO\n')
        time.sleep(0.1)
        response = p.read()
>       assert b'rebar' in response
E       AssertionError: assert 'rebar' in 'echo $FOO\r\n'

tests/test_spawn.py:15: AssertionError
______________________ PtyTestCase.test_spawn_unicode_sh _______________________

self = <tests.test_spawn.PtyTestCase testMethod=test_spawn_unicode_sh>

    def test_spawn_unicode_sh(self):
        env = os.environ.copy()
        env['FOO'] = 'rebar'
        p = PtyProcessUnicode.spawn(['sh'], env=env)
        p.read()
        p.write(u'echo $FOO\n')
        time.sleep(0.1)
        response = p.read()
>       assert u'rebar' in response
\n'     AssertionError: assert 'rebar' in 'echo $FOO

tests/test_spawn.py:31: AssertionError
====================== 2 failed, 3 passed in 0.80 seconds ======================

The expected output is produced, it is just not returned by the second p.read() in the test. Inserting a dummy p.read() before response = p.read() gets the tests passing.

I was not running tests on FreeBSD (10-Release) for ptyprocess (even though we do for pexpect). Addressing this now and we'll see how the tests do -- thanks for noticing!

Awesome find: it reproduces. I will address it, thanks @emaste !

Strange enough, output is seen three times with /bin/sh through ptyprocess.

We would expect it once for input (echo=True by default), and a second time for output. I'm not sure yet what is happening.

/bin/sh

In [8]: p = ptyprocess.PtyProcess.spawn(['sh'])

In [9]: p.write('echo HELLO; exit 0\n')
Out[9]: 19L

In [10]: p.read()
Out[10]: '(ptyprocess)[\\u@freebsd /home/freebsd/Code/ptyprocess]$ echo HELLO; exit 0\r\n'

In [11]: p.read()
Out[11]: 'echo HELLO; exit 0\r\n'

In [12]: p.read()
Out[12]: 'HELLO\r\n'

In [13]: p.read()
---------------------------------------------------------------------------
EOFError                                  Traceback (most recent call last)
<ipython-input-13-3cef27ad5fc2> in <module>()
----> 1 p.read()

/usr/home/freebsd/Code/ptyprocess/ptyprocess/ptyprocess.pyc in read(self, size)
    520             # BSD-style EOF (also appears to work on recent Solaris (OpenIndiana))
    521             self.flag_eof = True
--> 522             raise EOFError('End Of File (EOF). Empty string style platform.')
    523
    524         return s

EOFError: End Of File (EOF). Empty string style platform.

bash

In [2]: p = ptyprocess.PtyProcess.spawn(['bash'])

In [3]: p.write('echo HELLO; exit 0\n')
Out[3]: 19L

In [4]: p.read()
Out[4]: '(ptyprocess)[freebsd@freebsd ~/Code/ptyprocess]$ echo HELLO; exit 0\r\n'

In [5]: p.read()
Out[5]: 'HELLO\r\nexit\r\n'

In [6]: p.read()
---------------------------------------------------------------------------
EOFError                                  Traceback (most recent call last)
<ipython-input-6-3cef27ad5fc2> in <module>()
----> 1 p.read()

/usr/home/freebsd/Code/ptyprocess/ptyprocess/ptyprocess.pyc in read(self, size)
    520             # BSD-style EOF (also appears to work on recent Solaris (OpenIndiana))
    521             self.flag_eof = True
--> 522             raise EOFError('End Of File (EOF). Empty string style platform.')
    523
    524         return s

EOFError: End Of File (EOF). Empty string style platform.

This is very strange. It is some form of a race condition that is particular with the AT&T shell supplied by FreeBSD. I'm going to just write some kind of workaround for the test case: I don't believe it exposes any actual bug with ptyprocess.

Triggered on 2nd call:

$ while [ 0 ]; do python /tmp/test_freebsd.py | grep '^-stty' && break || echo -n '.'; done
.-stty -a; TEST=1; echo $TEST; echo $FOO; exit 0

Triggered on 81st call:

$ while [ 0 ]; do python /tmp/test_freebsd.py | grep '^-stty' && break || echo -n '.'; done
.................................................................................-stty -a; TEST=1; echo $TEST; echo $FOO; exit 0

test_freebsd.py is:

    from __future__ import print_function
    import difflib
    from ptyprocess.ptyprocess import PtyProcess

    cmd = 'stty -a; TEST=1; echo $TEST; echo $FOO; exit 0\n'
    env = {'PS1': '\$ ', 'FOO': 'bar'}
    def get_out(proc):
        out = ''
        while True:
            try:
               out += proc.read()
            except EOFError:
               return out

    bash = PtyProcess.spawn(['bash'], env=env, cwd='/')
    bash.write(cmd)
    out_bash = get_out(bash)

    attsh = PtyProcess.spawn(['sh'], env=env, cwd='/')
    attsh.write(cmd)
    out_attsh = get_out(attsh)

    map(print, difflib.unified_diff(
        out_attsh.splitlines(),
        out_bash.splitlines(),
        fromfile='AT&T compatible shell',
        tofile='bourne-again shell'))

There is something I am not able to understand about /bin/sh on freebsd: This is actually some kind of "second output of input echo" that occurs only with the AT&T-compatible /bin/sh on FreeBSD, and intermittently (a modified version of the test runs forever on OSX and Linux).

Closed by #20, new ptyprocess should come shortly.