Skip to content

Commit 6efe721

Browse files
committed
Adds some semblance of testing for bash completion. Tests the completion logic in the argcomplete function but doesn't test actual completion in bash.
1 parent 94156f8 commit 6efe721

File tree

3 files changed

+214
-2
lines changed

3 files changed

+214
-2
lines changed

cmd2/argcomplete_bridge.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
try:
55
# check if argcomplete is installed
66
import argcomplete
7-
except ImportError:
7+
except ImportError: # pragma: no cover
88
# not installed, skip the rest of the file
99
pass
1010

@@ -70,7 +70,7 @@ def tokens_for_completion(line, endidx):
7070
break
7171
except ValueError:
7272
# ValueError can be caused by missing closing quote
73-
if not quotes_to_try:
73+
if not quotes_to_try: # pragma: no cover
7474
# Since we have no more quotes to try, something else
7575
# is causing the parsing error. Return None since
7676
# this means the line is malformed.

tests/test_bashcompletion.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# coding=utf-8
2+
"""
3+
Unit/functional testing for argparse completer in cmd2
4+
5+
Copyright 2018 Eric Lin <[email protected]>
6+
Released under MIT license, see LICENSE file
7+
"""
8+
import os
9+
import pytest
10+
import sys
11+
from typing import List
12+
13+
from cmd2.argparse_completer import ACArgumentParser, AutoCompleter
14+
from cmd2.argcomplete_bridge import CompletionFinder
15+
16+
17+
actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew',
18+
'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac',
19+
'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman',
20+
'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee']
21+
22+
23+
def query_actors() -> List[str]:
24+
"""Simulating a function that queries and returns a completion values"""
25+
return actors
26+
27+
28+
@pytest.fixture
29+
def parser1():
30+
"""creates a argparse object to test completion against"""
31+
ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']
32+
33+
def _do_media_movies(self, args) -> None:
34+
if not args.command:
35+
self.do_help('media movies')
36+
else:
37+
print('media movies ' + str(args.__dict__))
38+
39+
def _do_media_shows(self, args) -> None:
40+
if not args.command:
41+
self.do_help('media shows')
42+
43+
if not args.command:
44+
self.do_help('media shows')
45+
else:
46+
print('media shows ' + str(args.__dict__))
47+
48+
media_parser = ACArgumentParser(prog='media')
49+
50+
media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type')
51+
52+
movies_parser = media_types_subparsers.add_parser('movies')
53+
movies_parser.set_defaults(func=_do_media_movies)
54+
55+
movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command')
56+
57+
movies_list_parser = movies_commands_subparsers.add_parser('list')
58+
59+
movies_list_parser.add_argument('-t', '--title', help='Title Filter')
60+
movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+',
61+
choices=ratings_types)
62+
movies_list_parser.add_argument('-d', '--director', help='Director Filter')
63+
movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append')
64+
65+
movies_add_parser = movies_commands_subparsers.add_parser('add')
66+
movies_add_parser.add_argument('title', help='Movie Title')
67+
movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types)
68+
movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True)
69+
movies_add_parser.add_argument('actor', help='Actors', nargs='*')
70+
71+
movies_commands_subparsers.add_parser('delete')
72+
73+
shows_parser = media_types_subparsers.add_parser('shows')
74+
shows_parser.set_defaults(func=_do_media_shows)
75+
76+
shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command')
77+
78+
shows_commands_subparsers.add_parser('list')
79+
80+
return media_parser
81+
82+
83+
# noinspection PyShadowingNames
84+
def test_bash_nocomplete(parser1):
85+
completer = CompletionFinder()
86+
result = completer(parser1, AutoCompleter(parser1))
87+
assert result is None
88+
89+
90+
# save the real os.fdopen
91+
os_fdopen = os.fdopen
92+
93+
94+
def my_fdopen(fd, mode):
95+
"""mock fdopen that redirects 8 and 9 from argcomplete to stdin/stdout for testing"""
96+
if fd > 7:
97+
return os_fdopen(fd - 7, mode)
98+
return os_fdopen(fd, mode)
99+
100+
101+
# noinspection PyShadowingNames
102+
def test_invalid_ifs(parser1, mock):
103+
completer = CompletionFinder()
104+
105+
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
106+
'_ARGCOMPLETE_IFS': '\013\013'})
107+
108+
mock.patch.object(os, 'fdopen', my_fdopen)
109+
110+
with pytest.raises(SystemExit):
111+
completer(parser1, AutoCompleter(parser1), exit_method=sys.exit)
112+
113+
114+
# noinspection PyShadowingNames
115+
@pytest.mark.parametrize('comp_line, exp_out, exp_err', [
116+
('media ', 'movies\013shows', ''),
117+
('media mo', 'movies', ''),
118+
('media movies add ', '\013\013 ', '''
119+
Hint:
120+
TITLE Movie Title'''),
121+
('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
122+
('media movies list ', '', '')
123+
])
124+
def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
125+
completer = CompletionFinder()
126+
127+
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
128+
'_ARGCOMPLETE_IFS': '\013',
129+
'COMP_TYPE': '63',
130+
'COMP_LINE': comp_line,
131+
'COMP_POINT': str(len(comp_line))})
132+
133+
mock.patch.object(os, 'fdopen', my_fdopen)
134+
135+
with pytest.raises(SystemExit):
136+
choices = {'actor': query_actors, # function
137+
}
138+
autocompleter = AutoCompleter(parser1, arg_choices=choices)
139+
completer(parser1, autocompleter, exit_method=sys.exit)
140+
141+
out, err = capfd.readouterr()
142+
assert out == exp_out
143+
assert err == exp_err
144+
145+
146+
def fdopen_fail_8(fd, mode):
147+
"""mock fdopen that forces failure if fd == 8"""
148+
if fd == 8:
149+
raise IOError()
150+
return my_fdopen(fd, mode)
151+
152+
153+
# noinspection PyShadowingNames
154+
def test_fail_alt_stdout(parser1, mock):
155+
completer = CompletionFinder()
156+
157+
comp_line = 'media movies list '
158+
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
159+
'_ARGCOMPLETE_IFS': '\013',
160+
'COMP_TYPE': '63',
161+
'COMP_LINE': comp_line,
162+
'COMP_POINT': str(len(comp_line))})
163+
mock.patch.object(os, 'fdopen', fdopen_fail_8)
164+
165+
try:
166+
choices = {'actor': query_actors, # function
167+
}
168+
autocompleter = AutoCompleter(parser1, arg_choices=choices)
169+
completer(parser1, autocompleter, exit_method=sys.exit)
170+
except SystemExit as err:
171+
assert err.code == 1
172+
173+
174+
def fdopen_fail_9(fd, mode):
175+
"""mock fdopen that forces failure if fd == 9"""
176+
if fd == 9:
177+
raise IOError()
178+
return my_fdopen(fd, mode)
179+
180+
181+
# noinspection PyShadowingNames
182+
def test_fail_alt_stderr(parser1, capfd, mock):
183+
completer = CompletionFinder()
184+
185+
comp_line = 'media movies add '
186+
exp_out = '\013\013 '
187+
exp_err = '''
188+
Hint:
189+
TITLE Movie Title'''
190+
191+
mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
192+
'_ARGCOMPLETE_IFS': '\013',
193+
'COMP_TYPE': '63',
194+
'COMP_LINE': comp_line,
195+
'COMP_POINT': str(len(comp_line))})
196+
mock.patch.object(os, 'fdopen', fdopen_fail_9)
197+
198+
with pytest.raises(SystemExit):
199+
choices = {'actor': query_actors, # function
200+
}
201+
autocompleter = AutoCompleter(parser1, arg_choices=choices)
202+
completer(parser1, autocompleter, exit_method=sys.exit)
203+
204+
out, err = capfd.readouterr()
205+
assert out == exp_out
206+
assert err == exp_err

tox.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ deps =
1515
pyperclip
1616
pytest
1717
pytest-cov
18+
pytest-mock
1819
wcwidth
1920
commands =
2021
py.test {posargs} --cov
@@ -25,6 +26,7 @@ deps =
2526
mock
2627
pyperclip
2728
pytest
29+
pytest-mock
2830
wcwidth
2931
commands = py.test -v
3032

@@ -33,6 +35,7 @@ deps =
3335
mock
3436
pyperclip
3537
pyreadline
38+
pytest-mock
3639
pytest
3740
commands = py.test -v
3841

@@ -42,6 +45,7 @@ deps =
4245
pyperclip
4346
pytest
4447
pytest-cov
48+
pytest-mock
4549
wcwidth
4650
commands =
4751
py.test {posargs} --cov
@@ -53,6 +57,7 @@ deps =
5357
pyperclip
5458
pyreadline
5559
pytest
60+
pytest-mock
5661
pytest-cov
5762
commands =
5863
py.test {posargs} --cov
@@ -62,6 +67,7 @@ commands =
6267
deps =
6368
pyperclip
6469
pytest
70+
pytest-mock
6571
wcwidth
6672
commands = py.test -v
6773

0 commit comments

Comments
 (0)