Skip to content

Commit a7c158a

Browse files
committed
Submenu code fully extracted and all tests pass
1 parent b8bd4b1 commit a7c158a

File tree

3 files changed

+105
-84
lines changed

3 files changed

+105
-84
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# -*- coding: utf-8 -*-
2+
# coding=utf-8
33

44
import os
55
import setuptools

tests/conftest.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#
2+
# coding=utf-8
3+
4+
class StdOut(object):
5+
""" Toy class for replacing self.stdout in cmd2.Cmd instances for unit testing. """
6+
def __init__(self):
7+
self.buffer = ''
8+
9+
def write(self, s):
10+
self.buffer += s
11+
12+
def read(self):
13+
raise NotImplementedError
14+
15+
def clear(self):
16+
self.buffer = ''
17+
18+
def normalize(block):
19+
""" Normalize a block of text to perform comparison.
20+
21+
Strip newlines from the very beginning and very end Then split into separate lines and strip trailing whitespace
22+
from each line.
23+
"""
24+
assert isinstance(block, str)
25+
block = block.strip('\n')
26+
return [line.rstrip() for line in block.splitlines()]
27+
28+
29+
def run_cmd(app, cmd):
30+
""" Clear StdOut buffer, run the command, extract the buffer contents, """
31+
app.stdout.clear()
32+
app.onecmd_plus_hooks(cmd)
33+
out = app.stdout.buffer
34+
app.stdout.clear()
35+
return normalize(out)

tests/test_submenu.py

Lines changed: 69 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1+
#
12
# coding=utf-8
23
"""
34
Cmd2 testing for argument parsing
45
"""
6+
import readline
7+
58
import pytest
9+
from unittest import mock
610

711
from cmd2 import cmd2
812
import cmd2_submenu
9-
from .conftest import run_cmd, StdOut, normalize
13+
from conftest import run_cmd, StdOut, normalize
1014

1115

1216
class SecondLevelB(cmd2.Cmd):
1317
"""To be used as a second level command class. """
1418

1519
def __init__(self, *args, **kwargs):
16-
super().__init__(self, *args, **kwargs)
20+
super().__init__(*args, **kwargs)
1721
self.prompt = '2ndLevel B '
1822

1923
def do_get_top_level_attr(self, line):
@@ -27,7 +31,7 @@ class SecondLevel(cmd2.Cmd):
2731
"""To be used as a second level command class. """
2832

2933
def __init__(self, *args, **kwargs):
30-
super().__init__(self, *args, **kwargs)
34+
super().__init__(*args, **kwargs)
3135
self.prompt = '2ndLevel '
3236
self.top_level_attr = None
3337

@@ -67,7 +71,7 @@ class SubmenuApp(cmd2.Cmd):
6771
"""To be used as the main / top level command class that will contain other submenus."""
6872

6973
def __init__(self, *args, **kwargs):
70-
super().__init__(self, *args, **kwargs)
74+
super().__init__(*args, **kwargs)
7175
self.prompt = 'TopLevel '
7276
self.top_level_attr = 123456789
7377

@@ -103,7 +107,6 @@ def secondlevel_app_b():
103107
app.stdout = StdOut()
104108
return app
105109

106-
107110
def run_submenu_cmd(app, second_level_app, cmd):
108111
""" Clear StdOut buffers, run the command, extract the buffer contents."""
109112
app.stdout.clear()
@@ -115,15 +118,52 @@ def run_submenu_cmd(app, second_level_app, cmd):
115118
second_level_app.stdout.clear()
116119
return normalize(out1), normalize(out2)
117120

121+
def complete_tester(text, line, begidx, endidx, app):
122+
"""
123+
This is a convenience function to test cmd2.complete() since
124+
in a unit test environment there is no actual console readline
125+
is monitoring. Therefore we use mock to provide readline data
126+
to complete().
127+
128+
:param text: str - the string prefix we are attempting to match
129+
:param line: str - the current input line with leading whitespace removed
130+
:param begidx: int - the beginning index of the prefix text
131+
:param endidx: int - the ending index of the prefix text
132+
:param app: the cmd2 app that will run completions
133+
:return: The first matched string or None if there are no matches
134+
Matches are stored in app.completion_matches
135+
These matches also have been sorted by complete()
136+
"""
137+
def get_line():
138+
return line
139+
140+
def get_begidx():
141+
return begidx
142+
143+
def get_endidx():
144+
return endidx
145+
146+
first_match = None
147+
with mock.patch.object(readline, 'get_line_buffer', get_line):
148+
with mock.patch.object(readline, 'get_begidx', get_begidx):
149+
with mock.patch.object(readline, 'get_endidx', get_endidx):
150+
# Run the readline tab-completion function with readline mocks in place
151+
first_match = app.complete(text, 0)
152+
153+
return first_match
118154

155+
######
156+
#
157+
# test submenu functionality
158+
#
159+
######
119160
def test_submenu_say_from_top_level(submenu_app):
120161
line = 'testing'
121162
out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'say ' + line)
122163
assert len(out1) == 1
123164
assert len(out2) == 0
124165
assert out1[0] == "You called a command in TopLevel with {!r}.".format(line)
125166

126-
127167
def test_submenu_second_say_from_top_level(submenu_app):
128168
line = 'testing'
129169
out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second say ' + line)
@@ -135,13 +175,11 @@ def test_submenu_second_say_from_top_level(submenu_app):
135175
assert len(out2) == 1
136176
assert out2[0] == "You called a command in SecondLevel with {!r}.".format(line)
137177

138-
139178
def test_submenu_say_from_second_level(secondlevel_app):
140179
line = 'testing'
141180
out = run_cmd(secondlevel_app, 'say ' + line)
142181
assert out == ["You called a command in SecondLevel with '%s'." % line]
143182

144-
145183
def test_submenu_help_second_say_from_top_level(submenu_app):
146184
out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say')
147185
# No output expected from the top level
@@ -150,29 +188,24 @@ def test_submenu_help_second_say_from_top_level(submenu_app):
150188
# Output expected from the second level
151189
assert out2 == ["This is a second level menu. Options are qwe, asd, zxc"]
152190

153-
154191
def test_submenu_help_say_from_second_level(secondlevel_app):
155192
out = run_cmd(secondlevel_app, 'help say')
156193
assert out == ["This is a second level menu. Options are qwe, asd, zxc"]
157194

158-
159195
def test_submenu_help_second(submenu_app):
160196
out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second')
161197
out3 = run_cmd(second_level_cmd, 'help')
162198
assert out2 == out3
163199

164-
165200
def test_submenu_from_top_help_second_say(submenu_app):
166201
out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say')
167202
out3 = run_cmd(second_level_cmd, 'help say')
168203
assert out2 == out3
169204

170-
171205
def test_submenu_shared_attribute(submenu_app):
172206
out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second get_top_level_attr')
173207
assert out2 == [str(submenu_app.top_level_attr)]
174208

175-
176209
def test_submenu_shared_attribute_preserve(submenu_app):
177210
out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr')
178211
assert out2 == [str(submenu_app.top_level_attr)]
@@ -184,127 +217,80 @@ def test_submenu_shared_attribute_preserve(submenu_app):
184217

185218
######
186219
#
187-
# from test_completion.py
220+
# test completion in submenus
188221
#
189222
######
190223

191-
####################################################
192-
193-
194-
class SecondLevel(cmd2.Cmd):
195-
"""To be used as a second level command class. """
196-
197-
def __init__(self, *args, **kwargs):
198-
super().__init__(self, *args, **kwargs)
199-
self.prompt = '2ndLevel '
200-
201-
def do_foo(self, line):
202-
self.poutput("You called a command in SecondLevel with '%s'. " % line)
203-
204-
def help_foo(self):
205-
self.poutput("This is a second level menu. Options are qwe, asd, zxc")
206-
207-
def complete_foo(self, text, line, begidx, endidx):
208-
return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
209-
210-
211-
second_level_cmd = SecondLevel()
212-
213-
214-
@cmd2_submenu.AddSubmenu(second_level_cmd,
215-
command='second',
216-
require_predefined_shares=False)
217-
class SubmenuApp(cmd2.Cmd):
218-
"""To be used as the main / top level command class that will contain other submenus."""
219-
220-
def __init__(self, *args, **kwargs):
221-
super().__init__(self, *args, **kwargs)
222-
self.prompt = 'TopLevel '
223-
224-
225-
@pytest.fixture
226-
def sb_app():
227-
app = SubmenuApp()
228-
return app
229-
230-
231-
def test_cmd2_submenu_completion_single_end(sb_app):
232-
text = 'f'
224+
def test_cmd2_submenu_completion_single_end(submenu_app):
225+
text = 'sa'
233226
line = 'second {}'.format(text)
234227
endidx = len(line)
235228
begidx = endidx - len(text)
236229

237-
first_match = complete_tester(text, line, begidx, endidx, sb_app)
230+
first_match = complete_tester(text, line, begidx, endidx, submenu_app)
238231

239232
# It is at end of line, so extra space is present
240-
assert first_match is not None and sb_app.completion_matches == ['foo ']
241-
233+
assert first_match is not None and submenu_app.completion_matches == ['say ']
242234

243-
def test_cmd2_submenu_completion_multiple(sb_app):
235+
def test_cmd2_submenu_completion_multiple(submenu_app):
244236
text = 'e'
245237
line = 'second {}'.format(text)
246238
endidx = len(line)
247239
begidx = endidx - len(text)
248240

249241
expected = ['edit', 'eof', 'eos']
250-
first_match = complete_tester(text, line, begidx, endidx, sb_app)
242+
first_match = complete_tester(text, line, begidx, endidx, submenu_app)
251243

252-
assert first_match is not None and sb_app.completion_matches == expected
244+
assert first_match is not None and submenu_app.completion_matches == expected
253245

254-
255-
def test_cmd2_submenu_completion_nomatch(sb_app):
246+
def test_cmd2_submenu_completion_nomatch(submenu_app):
256247
text = 'z'
257248
line = 'second {}'.format(text)
258249
endidx = len(line)
259250
begidx = endidx - len(text)
260251

261-
first_match = complete_tester(text, line, begidx, endidx, sb_app)
252+
first_match = complete_tester(text, line, begidx, endidx, submenu_app)
262253
assert first_match is None
263254

264-
265-
def test_cmd2_submenu_completion_after_submenu_match(sb_app):
255+
def test_cmd2_submenu_completion_after_submenu_match(submenu_app):
266256
text = 'a'
267-
line = 'second foo {}'.format(text)
257+
line = 'second say {}'.format(text)
268258
endidx = len(line)
269259
begidx = endidx - len(text)
270260

271-
first_match = complete_tester(text, line, begidx, endidx, sb_app)
272-
assert first_match is not None and sb_app.completion_matches == ['asd ']
273-
261+
first_match = complete_tester(text, line, begidx, endidx, submenu_app)
262+
assert first_match is not None and submenu_app.completion_matches == ['asd ']
274263

275-
def test_cmd2_submenu_completion_after_submenu_nomatch(sb_app):
264+
def test_cmd2_submenu_completion_after_submenu_nomatch(submenu_app):
276265
text = 'b'
277-
line = 'second foo {}'.format(text)
266+
line = 'second say {}'.format(text)
278267
endidx = len(line)
279268
begidx = endidx - len(text)
280269

281-
first_match = complete_tester(text, line, begidx, endidx, sb_app)
270+
first_match = complete_tester(text, line, begidx, endidx, submenu_app)
282271
assert first_match is None
283272

284-
285-
def test_cmd2_help_submenu_completion_multiple(sb_app):
273+
def test_cmd2_help_submenu_completion_multiple(submenu_app):
286274
text = 'p'
287275
line = 'help second {}'.format(text)
288276
endidx = len(line)
289277
begidx = endidx - len(text)
290278

291-
matches = sorted(sb_app.complete_help(text, line, begidx, endidx))
279+
matches = sorted(submenu_app.complete_help(text, line, begidx, endidx))
292280
assert matches == ['py', 'pyscript']
293281

294-
295-
def test_cmd2_help_submenu_completion_nomatch(sb_app):
282+
def test_cmd2_help_submenu_completion_nomatch(submenu_app):
296283
text = 'fake'
297284
line = 'help second {}'.format(text)
298285
endidx = len(line)
299286
begidx = endidx - len(text)
300-
assert sb_app.complete_help(text, line, begidx, endidx) == []
301-
287+
assert submenu_app.complete_help(text, line, begidx, endidx) == []
302288

303-
def test_cmd2_help_submenu_completion_subcommands(sb_app):
289+
def test_cmd2_help_submenu_completion_subcommands(submenu_app):
304290
text = 'p'
305291
line = 'help second {}'.format(text)
306292
endidx = len(line)
307293
begidx = endidx - len(text)
308294

309-
matches = sorted(sb_app.complete_help(text, line, begidx, endidx))
295+
matches = sorted(submenu_app.complete_help(text, line, begidx, endidx))
310296
assert matches == ['py', 'pyscript']

0 commit comments

Comments
 (0)