|
| 1 | +"""Unit tests for cmd2/pt_utils.py""" |
| 2 | + |
| 3 | +from typing import cast |
| 4 | +from unittest.mock import Mock |
| 5 | + |
| 6 | +import pytest |
| 7 | +from prompt_toolkit.document import Document |
| 8 | + |
| 9 | +from cmd2 import pt_utils, utils |
| 10 | +from cmd2.history import HistoryItem |
| 11 | +from cmd2.parsing import Statement |
| 12 | + |
| 13 | + |
| 14 | +# Mock for cmd2.Cmd |
| 15 | +class MockCmd: |
| 16 | + def __init__(self): |
| 17 | + self.complete = Mock() |
| 18 | + self.completion_matches = [] |
| 19 | + self.display_matches = [] |
| 20 | + self.history = [] |
| 21 | + |
| 22 | + |
| 23 | +@pytest.fixture |
| 24 | +def mock_cmd_app(): |
| 25 | + return MockCmd() |
| 26 | + |
| 27 | + |
| 28 | +class TestCmd2Completer: |
| 29 | + def test_get_completions_basic(self, mock_cmd_app): |
| 30 | + """Test basic completion without display matches.""" |
| 31 | + completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app)) |
| 32 | + |
| 33 | + # Setup document |
| 34 | + text = "foo" |
| 35 | + line = "command foo" |
| 36 | + cursor_position = len(line) |
| 37 | + document = Mock(spec=Document) |
| 38 | + document.get_word_before_cursor.return_value = text |
| 39 | + document.text = line |
| 40 | + document.cursor_position = cursor_position |
| 41 | + |
| 42 | + # Setup matches |
| 43 | + mock_cmd_app.completion_matches = ["foobar", "food"] |
| 44 | + mock_cmd_app.display_matches = [] # Empty means use completion matches for display |
| 45 | + |
| 46 | + # Call get_completions |
| 47 | + completions = list(completer.get_completions(document, None)) |
| 48 | + |
| 49 | + # Verify cmd_app.complete was called correctly |
| 50 | + # begidx = cursor_position - len(text) = 11 - 3 = 8 |
| 51 | + mock_cmd_app.complete.assert_called_once_with(text, 0, line=line, begidx=8, endidx=11, custom_settings=None) |
| 52 | + |
| 53 | + # Verify completions |
| 54 | + assert len(completions) == 2 |
| 55 | + assert completions[0].text == "foobar" |
| 56 | + assert completions[0].start_position == -3 |
| 57 | + # prompt_toolkit 3.0+ uses FormattedText for display |
| 58 | + assert completions[0].display == [('', 'foobar')] |
| 59 | + |
| 60 | + assert completions[1].text == "food" |
| 61 | + assert completions[1].start_position == -3 |
| 62 | + assert completions[1].display == [('', 'food')] |
| 63 | + |
| 64 | + def test_get_completions_with_display_matches(self, mock_cmd_app): |
| 65 | + """Test completion with display matches.""" |
| 66 | + completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app)) |
| 67 | + |
| 68 | + # Setup document |
| 69 | + text = "f" |
| 70 | + line = "f" |
| 71 | + document = Mock(spec=Document) |
| 72 | + document.get_word_before_cursor.return_value = text |
| 73 | + document.text = line |
| 74 | + document.cursor_position = 1 |
| 75 | + |
| 76 | + # Setup matches |
| 77 | + mock_cmd_app.completion_matches = ["foo", "bar"] |
| 78 | + mock_cmd_app.display_matches = ["Foo Display", "Bar Display"] |
| 79 | + |
| 80 | + # Call get_completions |
| 81 | + completions = list(completer.get_completions(document, None)) |
| 82 | + |
| 83 | + # Verify completions |
| 84 | + assert len(completions) == 2 |
| 85 | + assert completions[0].text == "foo" |
| 86 | + assert completions[0].display == [('', 'Foo Display')] |
| 87 | + |
| 88 | + assert completions[1].text == "bar" |
| 89 | + assert completions[1].display == [('', 'Bar Display')] |
| 90 | + |
| 91 | + def test_get_completions_mismatched_display_matches(self, mock_cmd_app): |
| 92 | + """Test completion when display_matches length doesn't match completion_matches.""" |
| 93 | + completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app)) |
| 94 | + |
| 95 | + document = Mock(spec=Document) |
| 96 | + document.get_word_before_cursor.return_value = "" |
| 97 | + document.text = "" |
| 98 | + document.cursor_position = 0 |
| 99 | + |
| 100 | + mock_cmd_app.completion_matches = ["foo", "bar"] |
| 101 | + mock_cmd_app.display_matches = ["Foo Display"] # Length mismatch |
| 102 | + |
| 103 | + completions = list(completer.get_completions(document, None)) |
| 104 | + |
| 105 | + # Should ignore display_matches and use completion_matches for display |
| 106 | + assert len(completions) == 2 |
| 107 | + assert completions[0].display == [('', 'foo')] |
| 108 | + assert completions[1].display == [('', 'bar')] |
| 109 | + |
| 110 | + def test_get_completions_empty(self, mock_cmd_app): |
| 111 | + """Test completion with no matches.""" |
| 112 | + completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app)) |
| 113 | + |
| 114 | + document = Mock(spec=Document) |
| 115 | + document.get_word_before_cursor.return_value = "" |
| 116 | + document.text = "" |
| 117 | + document.cursor_position = 0 |
| 118 | + |
| 119 | + mock_cmd_app.completion_matches = [] |
| 120 | + |
| 121 | + completions = list(completer.get_completions(document, None)) |
| 122 | + |
| 123 | + assert len(completions) == 0 |
| 124 | + |
| 125 | + def test_init_with_custom_settings(self, mock_cmd_app): |
| 126 | + """Test initializing with custom settings.""" |
| 127 | + mock_parser = Mock() |
| 128 | + custom_settings = utils.CustomCompletionSettings(parser=mock_parser) |
| 129 | + completer = pt_utils.Cmd2Completer(cast(any, mock_cmd_app), custom_settings=custom_settings) |
| 130 | + |
| 131 | + document = Mock(spec=Document) |
| 132 | + document.get_word_before_cursor.return_value = "" |
| 133 | + document.text = "" |
| 134 | + document.cursor_position = 0 |
| 135 | + |
| 136 | + mock_cmd_app.completion_matches = [] |
| 137 | + |
| 138 | + list(completer.get_completions(document, None)) |
| 139 | + |
| 140 | + mock_cmd_app.complete.assert_called_once() |
| 141 | + assert mock_cmd_app.complete.call_args[1]['custom_settings'] == custom_settings |
| 142 | + |
| 143 | + |
| 144 | +class TestCmd2History: |
| 145 | + def make_history_item(self, text): |
| 146 | + statement = Mock(spec=Statement) |
| 147 | + statement.raw = text |
| 148 | + item = Mock(spec=HistoryItem) |
| 149 | + item.statement = statement |
| 150 | + return item |
| 151 | + |
| 152 | + def test_load_history_strings(self, mock_cmd_app): |
| 153 | + """Test loading history strings in reverse order with deduping.""" |
| 154 | + history = pt_utils.Cmd2History(cast(any, mock_cmd_app)) |
| 155 | + |
| 156 | + # Setup history items |
| 157 | + # History in cmd2 is oldest to newest |
| 158 | + items = [ |
| 159 | + self.make_history_item("cmd1"), |
| 160 | + self.make_history_item("cmd2"), |
| 161 | + self.make_history_item("cmd2"), # Duplicate |
| 162 | + self.make_history_item("cmd3"), |
| 163 | + ] |
| 164 | + mock_cmd_app.history = items |
| 165 | + |
| 166 | + # Expected: cmd3, cmd2, cmd1 (duplicates removed) |
| 167 | + result = list(history.load_history_strings()) |
| 168 | + |
| 169 | + assert result == ["cmd3", "cmd2", "cmd1"] |
| 170 | + |
| 171 | + def test_load_history_strings_empty(self, mock_cmd_app): |
| 172 | + """Test loading history strings with empty history.""" |
| 173 | + history = pt_utils.Cmd2History(cast(any, mock_cmd_app)) |
| 174 | + |
| 175 | + mock_cmd_app.history = [] |
| 176 | + |
| 177 | + result = list(history.load_history_strings()) |
| 178 | + |
| 179 | + assert result == [] |
| 180 | + |
| 181 | + def test_get_strings(self, mock_cmd_app): |
| 182 | + """Test get_strings uses lazy loading.""" |
| 183 | + history = pt_utils.Cmd2History(cast(any, mock_cmd_app)) |
| 184 | + |
| 185 | + items = [self.make_history_item("test")] |
| 186 | + mock_cmd_app.history = items |
| 187 | + |
| 188 | + # Initially not loaded |
| 189 | + assert not history._loaded |
| 190 | + |
| 191 | + strings = history.get_strings() |
| 192 | + |
| 193 | + assert strings == ["test"] |
| 194 | + assert history._loaded |
| 195 | + assert history._loaded_strings == ["test"] |
| 196 | + |
| 197 | + # Call again, should return cached strings |
| 198 | + # Modify underlying history to prove it uses cache |
| 199 | + mock_cmd_app.history = [] |
| 200 | + strings2 = history.get_strings() |
| 201 | + assert strings2 == ["test"] |
| 202 | + |
| 203 | + def test_store_string(self, mock_cmd_app): |
| 204 | + """Test store_string does nothing.""" |
| 205 | + history = pt_utils.Cmd2History(cast(any, mock_cmd_app)) |
| 206 | + |
| 207 | + # Just ensure it doesn't raise error or modify cmd2 history |
| 208 | + history.store_string("new command") |
| 209 | + |
| 210 | + assert len(mock_cmd_app.history) == 0 |
0 commit comments