Skip to content

Commit f82c365

Browse files
committed
Improve unit test coverage for history
1 parent 1fe024f commit f82c365

File tree

2 files changed

+130
-67
lines changed

2 files changed

+130
-67
lines changed

cmd2/cmd2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3503,9 +3503,9 @@ def _initialize_history(self, hist_file):
35033503
# if the history file is in plain text format from 0.9.12 or lower
35043504
# this will fail, and the history in the plain text file will be lost
35053505
import atexit
3506-
atexit.register(self._persist_history_on_exit)
3506+
atexit.register(self._persist_history)
35073507

3508-
def _persist_history_on_exit(self):
3508+
def _persist_history(self):
35093509
"""write history out to the history file"""
35103510
if not self.persistent_history_file:
35113511
return

tests/test_history.py

Lines changed: 128 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,68 +8,41 @@
88

99
import pytest
1010

11-
# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
11+
# Python 3.5 had some regressions in the unitest.mock module, so use
12+
# 3rd party mock if available
1213
try:
1314
import mock
1415
except ImportError:
1516
from unittest import mock
1617

1718
import cmd2
1819
from .conftest import run_cmd, normalize, HELP_HISTORY
19-
from cmd2.parsing import Statement
20-
from cmd2.history import HistoryItem
2120

2221

23-
def test_base_help_history(base_app):
24-
out, err = run_cmd(base_app, 'help history')
25-
assert out == normalize(HELP_HISTORY)
26-
27-
def test_exclude_from_history(base_app, monkeypatch):
28-
# Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock
29-
base_app.editor = 'fooedit'
30-
31-
# Mock out the subprocess.Popen call so we don't actually open an editor
32-
m = mock.MagicMock(name='Popen')
33-
monkeypatch.setattr("subprocess.Popen", m)
34-
35-
# Run edit command
36-
run_cmd(base_app, 'edit')
37-
38-
# Run history command
39-
run_cmd(base_app, 'history')
40-
41-
# Verify that the history is empty
42-
out, err = run_cmd(base_app, 'history')
43-
assert out == []
44-
45-
# Now run a command which isn't excluded from the history
46-
run_cmd(base_app, 'help')
47-
48-
# And verify we have a history now ...
49-
out, err = run_cmd(base_app, 'history')
50-
expected = normalize(""" 1 help""")
51-
assert out == expected
22+
#
23+
# readline tests
24+
#
25+
def test_readline_remove_history_item(base_app):
26+
from cmd2.rl_utils import readline
27+
assert readline.get_current_history_length() == 0
28+
readline.add_history('this is a test')
29+
assert readline.get_current_history_length() == 1
30+
readline.remove_history_item(0)
31+
assert readline.get_current_history_length() == 0
5232

33+
#
34+
# test History() class
35+
#
5336
@pytest.fixture
5437
def hist():
55-
from cmd2.history import History
38+
from cmd2.parsing import Statement
39+
from cmd2.cmd2 import History, HistoryItem
5640
h = History([HistoryItem(Statement('', raw='first'), 1),
5741
HistoryItem(Statement('', raw='second'), 2),
5842
HistoryItem(Statement('', raw='third'), 3),
59-
HistoryItem(Statement('', raw='fourth'), 4)])
43+
HistoryItem(Statement('', raw='fourth'),4)])
6044
return h
6145

62-
def test_history_item():
63-
raw = 'help'
64-
stmt = Statement('', raw=raw)
65-
index = 1
66-
hi = HistoryItem(stmt, index)
67-
assert hi.statement == stmt
68-
assert hi.idx == index
69-
assert hi.statement.raw == raw
70-
assert str(hi) == raw
71-
72-
7346
def test_history_class_span(hist):
7447
for tryit in ['*', ':', '-', 'all', 'ALL']:
7548
assert hist.span(tryit) == hist
@@ -205,6 +178,46 @@ def test_history_max_length(hist):
205178
assert hist.get(1).statement.raw == 'third'
206179
assert hist.get(2).statement.raw == 'fourth'
207180

181+
#
182+
# test HistoryItem()
183+
#
184+
@pytest.fixture
185+
def histitem():
186+
from cmd2.parsing import Statement
187+
from cmd2.history import HistoryItem
188+
statement = Statement('history',
189+
raw='help history',
190+
command='help',
191+
arg_list=['history'],
192+
)
193+
histitem = HistoryItem(statement, 1)
194+
return histitem
195+
196+
def test_history_item_instantiate():
197+
from cmd2.parsing import Statement
198+
from cmd2.history import HistoryItem
199+
statement = Statement('history',
200+
raw='help history',
201+
command='help',
202+
arg_list=['history'],
203+
)
204+
with pytest.raises(TypeError):
205+
_ = HistoryItem()
206+
with pytest.raises(TypeError):
207+
_ = HistoryItem(idx=1)
208+
with pytest.raises(TypeError):
209+
_ = HistoryItem(statement=statement)
210+
with pytest.raises(TypeError):
211+
_ = HistoryItem(statement=statement, idx='hi')
212+
213+
def test_history_item_properties(histitem):
214+
assert histitem.raw == 'help history'
215+
assert histitem.expanded == 'help history'
216+
assert str(histitem) == 'help history'
217+
218+
#
219+
# test history command
220+
#
208221
def test_base_history(base_app):
209222
run_cmd(base_app, 'help')
210223
run_cmd(base_app, 'shortcuts')
@@ -283,7 +296,6 @@ def test_history_with_integer_argument(base_app):
283296
""")
284297
assert out == expected
285298

286-
287299
def test_history_with_integer_span(base_app):
288300
run_cmd(base_app, 'help')
289301
run_cmd(base_app, 'shortcuts')
@@ -441,21 +453,40 @@ def test_history_script_expanded(base_app):
441453
expected = ['alias create s shortcuts', 'shortcuts']
442454
assert out == expected
443455

456+
def test_base_help_history(base_app):
457+
out, err = run_cmd(base_app, 'help history')
458+
assert out == normalize(HELP_HISTORY)
444459

445-
#####
446-
#
447-
# readline tests
448-
#
449-
#####
450-
def test_readline_remove_history_item(base_app):
451-
from cmd2.rl_utils import readline
452-
assert readline.get_current_history_length() == 0
453-
readline.add_history('this is a test')
454-
assert readline.get_current_history_length() == 1
455-
readline.remove_history_item(0)
456-
assert readline.get_current_history_length() == 0
460+
def test_exclude_from_history(base_app, monkeypatch):
461+
# Set a fake editor just to make sure we have one. We aren't
462+
# really going to call it due to the mock
463+
base_app.editor = 'fooedit'
457464

465+
# Mock out the subprocess.Popen call so we don't actually open an editor
466+
m = mock.MagicMock(name='Popen')
467+
monkeypatch.setattr("subprocess.Popen", m)
468+
469+
# Run edit command
470+
run_cmd(base_app, 'edit')
458471

472+
# Run history command
473+
run_cmd(base_app, 'history')
474+
475+
# Verify that the history is empty
476+
out, err = run_cmd(base_app, 'history')
477+
assert out == []
478+
479+
# Now run a command which isn't excluded from the history
480+
run_cmd(base_app, 'help')
481+
482+
# And verify we have a history now ...
483+
out, err = run_cmd(base_app, 'history')
484+
expected = normalize(""" 1 help""")
485+
assert out == expected
486+
487+
#
488+
# test history initialization
489+
#
459490
@pytest.fixture(scope="session")
460491
def hist_file():
461492
fd, filename = tempfile.mkstemp(prefix='hist_file', suffix='.txt')
@@ -467,16 +498,25 @@ def hist_file():
467498
except FileNotFoundError:
468499
pass
469500

470-
def test_bad_history_file_path(capsys, request):
501+
def test_history_file_is_directory(capsys):
471502
with tempfile.TemporaryDirectory() as test_dir:
472503
# Create a new cmd2 app
473504
cmd2.Cmd(persistent_history_file=test_dir)
474505
_, err = capsys.readouterr()
475506
assert 'is a directory' in err
476507

508+
def test_history_file_permission_error(mocker, capsys):
509+
mock_open = mocker.patch('builtins.open')
510+
mock_open.side_effect = PermissionError
511+
512+
cmd2.Cmd(persistent_history_file='/tmp/doesntmatter')
513+
out, err = capsys.readouterr()
514+
assert not out
515+
assert 'can not read' in err
516+
477517
def test_history_file_conversion_no_truncate_on_init(hist_file, capsys):
478-
# test the code that converts a plain text history file to a pickle binary
479-
# history file
518+
# make sure we don't truncate the plain text history file on init
519+
# it shouldn't get converted to pickle format until we save history
480520

481521
# first we need some plain text commands in the history file
482522
with open(hist_file, 'w') as hfobj:
@@ -505,12 +545,11 @@ def test_history_populates_readline(hist_file):
505545
run_cmd(app, 'shortcuts')
506546
run_cmd(app, 'shortcuts')
507547
run_cmd(app, 'alias')
508-
509548
# call the private method which is registered to write history at exit
510-
app._persist_history_on_exit()
511-
# - create a new cmd2 with persistent history
512-
app = cmd2.Cmd(persistent_history_file=hist_file)
549+
app._persist_history()
513550

551+
# see if history came back
552+
app = cmd2.Cmd(persistent_history_file=hist_file)
514553
assert len(app.history) == 4
515554
assert app.history.get(1).statement.raw == 'help'
516555
assert app.history.get(2).statement.raw == 'shortcuts'
@@ -525,3 +564,27 @@ def test_history_populates_readline(hist_file):
525564
assert readline.get_history_item(1) == 'help'
526565
assert readline.get_history_item(2) == 'shortcuts'
527566
assert readline.get_history_item(3) == 'alias'
567+
568+
#
569+
# test cmd2's ability to write out history on exit
570+
# we are testing the _persist_history_on_exit() method, and
571+
# we assume that the atexit module will call this method
572+
# properly
573+
#
574+
def test_persist_history_ensure_no_error_if_no_histfile(base_app, capsys):
575+
# make sure if there is no persistent history file and someone
576+
# calls the private method call that we don't get an error
577+
base_app._persist_history()
578+
out, err = capsys.readouterr()
579+
assert not out
580+
assert not err
581+
582+
def test_persist_history_permission_error(hist_file, mocker, capsys):
583+
app = cmd2.Cmd(persistent_history_file=hist_file)
584+
run_cmd(app, 'help')
585+
mock_open = mocker.patch('builtins.open')
586+
mock_open.side_effect = PermissionError
587+
app._persist_history()
588+
out, err = capsys.readouterr()
589+
assert not out
590+
assert 'can not write' in err

0 commit comments

Comments
 (0)