Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interactive test using pexpect #9

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

theY4Kman
Copy link
Contributor

@theY4Kman theY4Kman commented May 27, 2019

I spent some time messing about to find a way to test fancycompleter's interactive features. This PR has an example utilizing pexpect to spawn a cmd-module-based REPL backed by fancycompleter.

I tested it on Linux and OSX. It fails on Linux if has_leopard_libedit is forced to return False.

The pexpect dep was purely for convenience, and could likely be replaced with direct usage of pty.

Hope it helps!

@codecov
Copy link

codecov bot commented May 27, 2019

Codecov Report

Merging #9 into master will increase coverage by 4.63%.
The diff coverage is 88.09%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master       #9      +/-   ##
==========================================
+ Coverage   73.65%   78.28%   +4.63%     
==========================================
  Files           2        4       +2     
  Lines         410      456      +46     
  Branches       46       50       +4     
==========================================
+ Hits          302      357      +55     
+ Misses         98       83      -15     
- Partials       10       16       +6
Impacted Files Coverage Δ
fancycompleter.py 69.18% <0%> (+3.93%) ⬆️
testing/test_interactive.py 100% <100%> (ø)
testing/fcomplete.py 75% <75%> (ø)
testing/test_fancycompleter.py 98.19% <0%> (+0.1%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 77638b6...6b7b6d9. Read the comment docs.

@blueyed
Copy link
Collaborator

blueyed commented May 27, 2019

Awesome, started using pexpect for an integration test with pdbpp also: pdbpp/pdbpp#18

@blueyed
Copy link
Collaborator

blueyed commented May 27, 2019

Added the missing pexpect dep to tox.ini.

@blueyed
Copy link
Collaborator

blueyed commented May 27, 2019

Apparently the libedit method fails on PyPy?! https://travis-ci.com/pdbpp/fancycompleter/jobs/203388229#L260

@theY4Kman
Copy link
Contributor Author

Interesting! I added the time.sleep only after testing on OSX, where it failed without it. I wonder if it may be more robust to run a select() loop for so many milliseconds while collecting those three lines.

Unrelated note, before I forget: the output differed between Linux and OSX. Here's linux:

'complete_\x07\r\n            complete_a  complete_b  complete_c  \r\ncomplete_'

And OSX:

'complete_complete_\x07\r\x1b[10G\r\n            complete_a  complete_b  complete_c \r\n\r\x1b[Kcomplete_'

@blueyed
Copy link
Collaborator

blueyed commented May 27, 2019

btw: came up with the following diff when I've thought the py27 failure was due to a timeout/delay:

diff --git a/testing/test_interactive.py b/testing/test_interactive.py
index 9e5f743..9c66f13 100644
--- a/testing/test_interactive.py
+++ b/testing/test_interactive.py
@@ -1,5 +1,4 @@
 import os.path
-import time
 
 import pexpect
 
@@ -16,27 +15,19 @@ def test_global_matches():
     args = [FCOMPLETE_PATH]
     args.extend(full_names)
 
-    fcomplete = pexpect.spawn('python', args)
-
-    try:
+    with pexpect.spawn('python', args, timeout=5) as fcomplete:
         fcomplete.send(prefix)
-        output = fcomplete.read_nonblocking(1000).decode('utf-8')
-        assert output == prefix
+        fcomplete.expect_exact(prefix)
 
         fcomplete.send('\t')
-        fcomplete.read_nonblocking(1000).decode('utf-8')
+        # NOTE: on py27 it is:
+        # fcomplete.expect_exact('\tcomplete_\x07')
+        # But on py37 only:
+        fcomplete.expect_exact('\x07')
 
         fcomplete.send('\t')
-        time.sleep(0.1)
-        output = fcomplete.read_nonblocking(1000).decode('utf-8')
-        lines = output.split('\r\n')
-
-        assert len(lines) == 3
-        assert lines[2].endswith(prefix)
-
-        assert all(full_name in lines[1] for full_name in full_names)
-        completed_names = lines[1].strip().split('  ')
-        assert set(completed_names) == set(full_names)
-
-    finally:
-        fcomplete.terminate(force=True)
+        fcomplete.expect_exact(
+            '\r\n'
+            '            complete_a  complete_b  complete_c  '
+            '\r\ncomplete_'
+        )

While some of this is good, it is actually better to compare/assert the actual (whole) output like you do it.

@blueyed
Copy link
Collaborator

blueyed commented May 27, 2019

The py27 failure ("setupterm failed") reminded me of pypy/pyrepl#8.

@blueyed
Copy link
Collaborator

blueyed commented May 27, 2019

the output differed between Linux and OSX

This might also be due to TERM - with pdbpp I'm setting TERM=xterm-256color always / explicitly: https://github.com/pdbpp/pdbpp/blob/c9318c31de813132c0564acd8831c8dda3fbc0d1/testing/conftest.py#L14-L22

@theY4Kman
Copy link
Contributor Author

theY4Kman commented May 28, 2019

AFAIK, curses reads info from a terminfo database to figure out how to interact with the terminal. Without TERM set, it doesn't know what to look up.

The ncurses API docs (found through this SO answer) state an err.value of -1 from setupterm()

means that the terminfo database could not be found.

The C source shows a little more detail

tname = getenv("TERM");
if (tname == 0 || *tname == '\0') {
    ret_error0(TGETENT_ERR, "TERM environment variable not set.\n");
}

curses.priv.h shows

#define TGETENT_ERR -1		/* an error occurred */

EDIT: btw, ret_error0 at the most just prints out that message – it doesn't make it available to the caller. Since errret is passed, it don't do sheeeit with the message

#define ret_error0(code, msg)		if (errret) {\
					    *errret = code;\
					    returnCode(ERR);\
					} else {\
					    fprintf(stderr, msg);\
					    exit(EXIT_FAILURE);\
					}

@theY4Kman
Copy link
Contributor Author

Didn't know about the contextmanager on pexpect.spawn; that's handy

@theY4Kman
Copy link
Contributor Author

Well, I dug in a bit deeper. tl;dr no solution, yet, but my eyes are on pyrepl.

First, I tried all the possible values of TERM on my system w/ pypy/3. None of them seemed to print out nice, parseable, American-made \r's for carriage returns. They all seem to lay the place to waste with control codes up the wazoo. (I did learn a lot on the Zen Terminal Escape Codes page, though. Things sure were a lot more complicated before curses.)

Honestly, I hadn't used PyPy very much before, so it wasn't till I started printf debugging ('cause hooking up the PyCharm debugger to a pexpect child seemed like a rabbit hole for a different 1AM night) before I realized pyrepl is always used under PyPy — and shortly after it clicked: PyPy doesn't have a real readline; it just uses pyrepl for its readline. The same pyrepl I was avoiding with prefer_pyrepl = False, because I was never able to squeeze a nicely packaged output from it.

I was getting things like

�[?1h�=�[?25l�[1A
�[?12l�[?25h�[1@c�[1@o�[1@m�[1@p�[1@l�[1@e�[1@t�[1@e�[1@_�[?25l�[9D
[ not unique ]�[?12l�[?25h�[5D�[1A�[?25l�[9D            complete_a  complete_b  complete_c                          �[72D�[1B�[Kcomplete_�[?12l�[?25h

(Though, it was the [ not unique ] that tipped me off to pyrepl)

I wonder if there might be some way to massage pyrepl's readline into speaking normalspeak. Maybe chunking the characters written or read in some fashion would speak some sense into the thing. Maybe just interpreting the escape codes :O

I have no idea what's gonna happen next, and I'm loving it.

@blueyed
Copy link
Collaborator

blueyed commented Oct 28, 2019

Would be great to pick this up / finish it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants