Skip to content

Commit a2ab87a

Browse files
committed
test(pt_utils): achieve 100% coverage for pt_utils.py
Added unit tests for completion hints, hint tables, custom delimiter logic, and CompletionItem metadata handling in pt_utils.py. Achieved 100% code coverage (excluding TYPE_CHECKING blocks) and modernized existing tests to use real Document objects.
1 parent 19d58ff commit a2ab87a

File tree

1 file changed

+77
-31
lines changed

1 file changed

+77
-31
lines changed

tests/test_pt_utils.py

Lines changed: 77 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Unit tests for cmd2/pt_utils.py"""
22

3-
from typing import cast
3+
from typing import Any, cast
44
from unittest.mock import Mock
55

66
import pytest
77
from prompt_toolkit.document import Document
88

99
from cmd2 import pt_utils, utils
10+
from cmd2.argparse_custom import CompletionItem
1011
from cmd2.history import HistoryItem
1112
from cmd2.parsing import Statement
1213

@@ -20,6 +21,8 @@ def __init__(self):
2021
self.history = []
2122
self.formatted_completions = ''
2223
self.completion_hint = ''
24+
self.statement_parser = Mock()
25+
self.statement_parser.terminators = [';']
2326

2427

2528
@pytest.fixture
@@ -30,16 +33,13 @@ def mock_cmd_app():
3033
class TestCmd2Completer:
3134
def test_get_completions_basic(self, mock_cmd_app):
3235
"""Test basic completion without display matches."""
33-
completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app))
36+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app))
3437

3538
# Setup document
3639
text = "foo"
3740
line = "command foo"
3841
cursor_position = len(line)
39-
document = Mock(spec=Document)
40-
document.get_word_before_cursor.return_value = text
41-
document.text = line
42-
document.cursor_position = cursor_position
42+
document = Document(line, cursor_position=cursor_position)
4343

4444
# Setup matches
4545
mock_cmd_app.completion_matches = ["foobar", "food"]
@@ -65,15 +65,11 @@ def test_get_completions_basic(self, mock_cmd_app):
6565

6666
def test_get_completions_with_display_matches(self, mock_cmd_app):
6767
"""Test completion with display matches."""
68-
completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app))
68+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app))
6969

7070
# Setup document
71-
text = "f"
7271
line = "f"
73-
document = Mock(spec=Document)
74-
document.get_word_before_cursor.return_value = text
75-
document.text = line
76-
document.cursor_position = 1
72+
document = Document(line, cursor_position=1)
7773

7874
# Setup matches
7975
mock_cmd_app.completion_matches = ["foo", "bar"]
@@ -92,12 +88,9 @@ def test_get_completions_with_display_matches(self, mock_cmd_app):
9288

9389
def test_get_completions_mismatched_display_matches(self, mock_cmd_app):
9490
"""Test completion when display_matches length doesn't match completion_matches."""
95-
completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app))
91+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app))
9692

97-
document = Mock(spec=Document)
98-
document.get_word_before_cursor.return_value = ""
99-
document.text = ""
100-
document.cursor_position = 0
93+
document = Document("", cursor_position=0)
10194

10295
mock_cmd_app.completion_matches = ["foo", "bar"]
10396
mock_cmd_app.display_matches = ["Foo Display"] # Length mismatch
@@ -111,12 +104,9 @@ def test_get_completions_mismatched_display_matches(self, mock_cmd_app):
111104

112105
def test_get_completions_empty(self, mock_cmd_app):
113106
"""Test completion with no matches."""
114-
completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app))
107+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app))
115108

116-
document = Mock(spec=Document)
117-
document.get_word_before_cursor.return_value = ""
118-
document.text = ""
119-
document.cursor_position = 0
109+
document = Document("", cursor_position=0)
120110

121111
mock_cmd_app.completion_matches = []
122112

@@ -128,12 +118,9 @@ def test_init_with_custom_settings(self, mock_cmd_app):
128118
"""Test initializing with custom settings."""
129119
mock_parser = Mock()
130120
custom_settings = utils.CustomCompletionSettings(parser=mock_parser)
131-
completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app), custom_settings=custom_settings)
121+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app), custom_settings=custom_settings)
132122

133-
document = Mock(spec=Document)
134-
document.get_word_before_cursor.return_value = ""
135-
document.text = ""
136-
document.cursor_position = 0
123+
document = Document("", cursor_position=0)
137124

138125
mock_cmd_app.completion_matches = []
139126

@@ -142,6 +129,65 @@ def test_init_with_custom_settings(self, mock_cmd_app):
142129
mock_cmd_app.complete.assert_called_once()
143130
assert mock_cmd_app.complete.call_args[1]['custom_settings'] == custom_settings
144131

132+
def test_get_completions_with_hints(self, mock_cmd_app, monkeypatch):
133+
"""Test that hints and formatted completions are printed even with no matches."""
134+
mock_print = Mock()
135+
monkeypatch.setattr(pt_utils, "print_formatted_text", mock_print)
136+
137+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app))
138+
document = Document("test", cursor_position=4)
139+
140+
mock_cmd_app.formatted_completions = "Table Data"
141+
mock_cmd_app.completion_hint = "Hint Text"
142+
mock_cmd_app.completion_matches = []
143+
144+
list(completer.get_completions(document, None))
145+
146+
assert mock_print.call_count == 2
147+
assert mock_cmd_app.formatted_completions == ""
148+
assert mock_cmd_app.completion_hint == ""
149+
150+
def test_get_completions_completion_item_meta(self, mock_cmd_app):
151+
"""Test that CompletionItem descriptive data is used as display_meta."""
152+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app))
153+
document = Document("foo", cursor_position=3)
154+
155+
# item1 with desc, item2 without desc
156+
item1 = CompletionItem("foobar", ["My Description"])
157+
item2 = CompletionItem("food", [])
158+
mock_cmd_app.completion_matches = [item1, item2]
159+
160+
completions = list(completer.get_completions(document, None))
161+
162+
assert len(completions) == 2
163+
assert completions[0].text == "foobar"
164+
# display_meta is converted to FormattedText
165+
assert completions[0].display_meta == [('', 'My Description')]
166+
assert completions[1].display_meta == [('', '')]
167+
168+
def test_get_completions_no_statement_parser(self, mock_cmd_app):
169+
"""Test initialization and completion without statement_parser."""
170+
del mock_cmd_app.statement_parser
171+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app))
172+
173+
document = Document("foo bar", cursor_position=7)
174+
list(completer.get_completions(document, None))
175+
176+
# Should still work with default delimiters
177+
mock_cmd_app.complete.assert_called_once()
178+
179+
def test_get_completions_custom_delimiters(self, mock_cmd_app):
180+
"""Test that custom delimiters (terminators) are respected."""
181+
mock_cmd_app.statement_parser.terminators = ['#']
182+
completer = pt_utils.Cmd2Completer(cast(Any, mock_cmd_app))
183+
184+
# '#' should act as a word boundary
185+
document = Document("cmd#arg", cursor_position=7)
186+
list(completer.get_completions(document, None))
187+
188+
# text should be "arg", begidx=4, endidx=7
189+
mock_cmd_app.complete.assert_called_with("arg", 0, line="cmd#arg", begidx=4, endidx=7, custom_settings=None)
190+
145191

146192
class TestCmd2History:
147193
def make_history_item(self, text):
@@ -153,7 +199,7 @@ def make_history_item(self, text):
153199

154200
def test_load_history_strings(self, mock_cmd_app):
155201
"""Test loading history strings yields all items in forward order."""
156-
history = pt_utils.Cmd2History(cast(any, mock_cmd_app))
202+
history = pt_utils.Cmd2History(cast(Any, mock_cmd_app))
157203

158204
# Setup history items
159205
# History in cmd2 is oldest to newest
@@ -172,7 +218,7 @@ def test_load_history_strings(self, mock_cmd_app):
172218

173219
def test_load_history_strings_empty(self, mock_cmd_app):
174220
"""Test loading history strings with empty history."""
175-
history = pt_utils.Cmd2History(cast(any, mock_cmd_app))
221+
history = pt_utils.Cmd2History(cast(Any, mock_cmd_app))
176222

177223
mock_cmd_app.history = []
178224

@@ -182,7 +228,7 @@ def test_load_history_strings_empty(self, mock_cmd_app):
182228

183229
def test_get_strings(self, mock_cmd_app):
184230
"""Test get_strings returns deduped strings and does not cache."""
185-
history = pt_utils.Cmd2History(cast(any, mock_cmd_app))
231+
history = pt_utils.Cmd2History(cast(Any, mock_cmd_app))
186232

187233
items = [
188234
self.make_history_item("cmd1"),
@@ -203,7 +249,7 @@ def test_get_strings(self, mock_cmd_app):
203249

204250
def test_store_string(self, mock_cmd_app):
205251
"""Test store_string does nothing."""
206-
history = pt_utils.Cmd2History(cast(any, mock_cmd_app))
252+
history = pt_utils.Cmd2History(cast(Any, mock_cmd_app))
207253

208254
# Just ensure it doesn't raise error or modify cmd2 history
209255
history.store_string("new command")

0 commit comments

Comments
 (0)