1010import pytest
1111
1212import cmd2
13+ from cmd2 .history import HistoryIndexError
1314
1415from .conftest import (
1516 normalize ,
@@ -230,7 +231,7 @@ def test_history_class_span(hist) -> None:
230231
231232@pytest .mark .parametrize ("suffix" , [":" , ":999" , ":-1" , ".." , "..999" , "..-1" ])
232233def test_history_span_past_end (hist , suffix ) -> None :
233- with pytest .raises (IndexError , match = "No history items in range" ):
234+ with pytest .raises (HistoryIndexError , match = "No history items in range" ):
234235 hist .span (f"{ len (hist ) + 1 } { suffix } " )
235236
236237
@@ -244,26 +245,36 @@ def test_history_span_boundaries(hist, span, indices) -> None:
244245
245246@pytest .mark .parametrize ("span" , ["999:1" , "3:1" , "2:-4" , "1:-999" ])
246247def test_history_span_explicit_start_without_matches (hist , span ) -> None :
247- with pytest .raises (IndexError , match = "No history items in range" ):
248+ with pytest .raises (HistoryIndexError , match = "No history items in range" ):
248249 hist .span (span )
249250
250251
251252@pytest .mark .parametrize ("span" , ["1:" , "-1:" ])
252253def test_history_span_explicit_start_on_empty_history (hist , span ) -> None :
253254 hist .clear ()
254- with pytest .raises (IndexError , match = "No history items in range" ):
255+ with pytest .raises (HistoryIndexError , match = "No history items in range" ):
255256 hist .span (span )
256257
257258
258- @pytest .mark .parametrize ("span" , [":" , ".." , ":2" , ":-999" ])
259+ @pytest .mark .parametrize ("span" , [":" , ".." ])
259260@pytest .mark .parametrize ("empty_history" , [False , True ])
260- def test_history_span_open_start_without_matches (hist , span , empty_history ) -> None :
261+ def test_history_span_unbounded_without_matches (hist , span , empty_history ) -> None :
261262 if empty_history :
262263 hist .clear ()
263264 hist .start_session ()
264265 assert hist .span (span ) == {}
265266
266267
268+ @pytest .mark .parametrize ("span" , [":2" , "..2" , "..-3" , ":-999" ])
269+ @pytest .mark .parametrize ("empty_history" , [False , True ])
270+ def test_history_span_end_only_without_matches (hist , span , empty_history ) -> None :
271+ if empty_history :
272+ hist .clear ()
273+ hist .start_session ()
274+ with pytest .raises (HistoryIndexError , match = "No history items in range" ):
275+ hist .span (span )
276+
277+
267278def test_persisted_history_span (persisted_hist ) -> None :
268279 span = persisted_hist .span ("2.." )
269280 assert len (span ) == 5
@@ -320,14 +331,33 @@ def test_history_class_get(hist) -> None:
320331 assert hist .get (3 ).statement .raw == "third"
321332 assert hist .get (- 2 ) == hist [- 2 ]
322333 assert hist .get (- 1 ).statement .raw == "fourth"
334+ assert hist .get (len (hist )) == hist [- 1 ]
335+ assert hist .get (- len (hist )) == hist [0 ]
323336
324- with pytest .raises (IndexError ):
337+ with pytest .raises (HistoryIndexError , match = "No history items in range" ):
325338 hist .get (0 )
326339
327- with pytest .raises (IndexError ):
340+ with pytest .raises (HistoryIndexError , match = "No history items in range" ):
328341 hist .get (5 )
329342
330343
344+ @pytest .mark .parametrize ("index" , [0 , 5 , - 5 , 10 ** 100 , - (10 ** 100 )])
345+ @pytest .mark .parametrize ("empty_history" , [False , True ])
346+ def test_history_get_invalid_index_has_clear_error (hist , index , empty_history ) -> None :
347+ if empty_history :
348+ hist .clear ()
349+ with pytest .raises (HistoryIndexError , match = "No history items in range" ) as error :
350+ hist .get (index )
351+ assert isinstance (error .value , IndexError )
352+
353+
354+ @pytest .mark .parametrize ("index" , [1 , - 1 ])
355+ def test_history_get_on_empty_history (hist , index ) -> None :
356+ hist .clear ()
357+ with pytest .raises (HistoryIndexError , match = "No history items in range" ):
358+ hist .get (index )
359+
360+
331361def test_history_str_search (hist ) -> None :
332362 items = hist .str_search ("ir" )
333363 assert len (items ) == 2
@@ -681,12 +711,14 @@ def test_history_with_span_end(base_app) -> None:
681711
682712
683713# Each of these selects nothing when history holds a single item
684- EMPTY_HISTORY_SPANS = ["2" , "5" , "999" , "-2" , "2:" , "2:3" , "2.." , "2..3" , "2:1" ]
714+ EMPTY_HISTORY_SPANS = ["0" , " 2" , "5" , "999" , "-2" , "2:" , "2:3" , "2.." , "2..3" , "2:1" , ":-999 " ]
685715
686716
687- @pytest .mark .parametrize ("selector" , ["" , ":" , ".." , ":2" ])
717+ @pytest .mark .parametrize (
718+ ("selector" , "empty_selection" ), [("" , False ), (":" , False ), (".." , False ), (":2" , True ), ("..-3" , True )]
719+ )
688720@pytest .mark .parametrize ("empty_history" , [False , True ])
689- def test_history_open_start_on_fresh_session (tmp_path , selector , empty_history ) -> None :
721+ def test_history_selections_on_fresh_session (tmp_path , selector , empty_selection , empty_history ) -> None :
690722 history_file = str (tmp_path / "history.dat" )
691723 previous_app = cmd2 .Cmd (persistent_history_file = history_file )
692724 if not empty_history :
@@ -699,13 +731,17 @@ def test_history_open_start_on_fresh_session(tmp_path, selector, empty_history)
699731 out , err = run_cmd (app , f"history { selector } " )
700732
701733 assert out == []
702- assert err == []
703- assert app .last_result == {}
734+ if empty_selection :
735+ assert err == ["No history items in range" ]
736+ assert app .last_result is False
737+ else :
738+ assert err == []
739+ assert app .last_result == {}
704740
705741
706742@pytest .mark .parametrize ("action" , ["--run" , "--edit" , "--output-file" ])
707- @pytest .mark .parametrize ("selector" , [":" , ".." , ":2" ])
708- def test_history_open_start_empty_actions (base_app , hist , monkeypatch , tmp_path , action , selector ) -> None :
743+ @pytest .mark .parametrize ("selector" , [":" , ".." ])
744+ def test_history_unbounded_empty_actions (base_app , hist , monkeypatch , tmp_path , action , selector ) -> None :
709745 base_app .history = hist
710746 hist .start_session ()
711747 output_file = tmp_path / "out.txt"
@@ -732,11 +768,29 @@ def test_history_open_start_empty_actions(base_app, hist, monkeypatch, tmp_path,
732768 assert base_app .last_result is True
733769
734770
735- @pytest .mark .parametrize ("span" , ["3:1" , "2:-4" , "1:-999" ])
771+ @pytest .mark .parametrize ("span" , [":2" , "..2" , "..-3" ])
772+ def test_history_end_only_with_all_includes_persisted (base_app , hist , span ) -> None :
773+ base_app .history = hist
774+ hist .start_session ()
775+
776+ out , err = run_cmd (base_app , f"history --all { span } " )
777+
778+ assert len (out ) == 2
779+ assert err == []
780+ assert list (base_app .last_result ) == [1 , 2 ]
781+
782+
783+ @pytest .mark .parametrize (
784+ ("span" , "session_start" ),
785+ [("3:1" , 0 ), ("2:-4" , 0 ), ("1:-999" , 0 ), (":1" , 2 ), ("..2" , 2 ), ("..-3" , 2 ), (":2" , 4 ), ("..2" , 4 ), ("..-3" , 4 )],
786+ )
736787@pytest .mark .parametrize ("action" , ["" , "--run" , "--edit" , "--output-file" ])
737788@pytest .mark .parametrize ("debug" , [False , True ])
738- def test_history_reversed_span_has_no_side_effects (base_app , hist , monkeypatch , tmp_path , span , action , debug ) -> None :
789+ def test_history_empty_bounded_span_has_no_side_effects (
790+ base_app , hist , monkeypatch , tmp_path , span , session_start , action , debug
791+ ) -> None :
739792 base_app .history = hist
793+ hist .session_start_index = session_start
740794 base_app .debug = debug
741795 mkstemp_mock = mock .MagicMock (name = "mkstemp" )
742796 monkeypatch .setattr ("cmd2.cmd2.tempfile.mkstemp" , mkstemp_mock )
@@ -905,9 +959,29 @@ def test_history_search_without_match_is_not_an_error(debug_history_app, search)
905959 assert debug_history_app .last_result == {}
906960
907961
908- def test_history_index_zero_error_unchanged (debug_history_app ) -> None :
909- with pytest .raises (IndexError , match = r"The first command in history is command 1\." ):
910- debug_history_app .onecmd ("history 0" )
962+ @pytest .mark .parametrize ("debug" , [False , True ])
963+ def test_history_index_zero_has_clean_error (debug_history_app , debug ) -> None :
964+ debug_history_app .debug = debug
965+ out , err = run_cmd (debug_history_app , "history 0" )
966+ assert out == []
967+ assert err == ["No history items in range" ]
968+ assert debug_history_app .last_result is False
969+
970+
971+ @pytest .mark .parametrize ("selector" , ["1" , "1:" ])
972+ @pytest .mark .parametrize ("debug" , [False , True ])
973+ def test_history_does_not_hide_unexpected_index_errors (base_app , hist , monkeypatch , selector , debug ) -> None :
974+ base_app .history = hist
975+ base_app .debug = debug
976+ monkeypatch .setattr (type (hist ), "__getitem__" , mock .Mock (side_effect = IndexError ("unexpected lookup failure" )))
977+
978+ with pytest .raises (IndexError , match = "unexpected lookup failure" ):
979+ base_app .onecmd (f"history { selector } " )
980+
981+ out , err = run_cmd (base_app , f"history { selector } " )
982+ assert out == []
983+ assert err != ["No history items in range" ]
984+ assert any ("unexpected lookup failure" in line for line in err )
911985
912986
913987def test_history_with_span_index_error (base_app ) -> None :
0 commit comments