Skip to content

Commit afda9e8

Browse files
committed
AppTests: Full test cov for app
Signed-off-by: Gabe Goodhart <[email protected]>
1 parent 27dc5e1 commit afda9e8

File tree

2 files changed

+192
-4
lines changed

2 files changed

+192
-4
lines changed

scriptit/app.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
class TerminalApp:
3434
__doc__ = __doc__
3535

36+
CONSOLE_START = "== CONSOLE "
37+
3638
## Construction ##############################################################
3739

3840
def __init__(
@@ -77,7 +79,6 @@ def __init__(
7779
self.log_string_output, self.log_file_handle
7880
)
7981
self._wrap_all_logging(preserve_log_handlers)
80-
self._log = logging.getLogger("APP")
8182

8283
# Set up a buffer to store non-log lines in
8384
self.previous_content_entities = []
@@ -115,11 +116,19 @@ def _wrap_all_logging(self, preserve_log_handlers: bool):
115116
)
116117

117118
# Update all existing handlers
119+
# NOTE: The choice here to update _all_ handlers is based on the
120+
# assumption that a user will be unlikely to configure multiple
121+
# handlers when running a terminal app. If they do, log lines will end
122+
# up duplicated for each handler. The alternative is to attempt to
123+
# decide _which_ of the multiple handlers should be wrapped, but this
124+
# gets further complicated by needing to handle future handlers, so
125+
# the simpler choice is to just let this be a user problem.
118126
for logger in [logging.root] + list(logging.root.manager.loggerDict.values()):
119127
if isinstance(logger, logging.PlaceHolder):
120128
continue
121-
for i, handler in enumerate(logger.handlers):
122-
logger.handlers[i] = make_wrapped_handler(handler)
129+
if logger.handlers:
130+
for i, handler in enumerate(logger.handlers):
131+
logger.handlers[i] = make_wrapped_handler(handler)
123132

124133
# When new loggers are set up and have handlers directly configured,
125134
# intercept them and wrap the handlers
@@ -155,7 +164,7 @@ def _refresh(self, force, use_previous):
155164
content_height = height - log_height
156165

157166
# Add the log console
158-
heading = "== CONSOLE "
167+
heading = self.CONSOLE_START
159168
raw_log_lines = filter(
160169
lambda line: bool(line.strip()),
161170
self.log_string_output.getvalue().split("\n"),
@@ -166,6 +175,8 @@ def _refresh(self, force, use_previous):
166175
log_lines.append(line[:width])
167176
line = line[width:]
168177
log_lines.append(line)
178+
# DEBUG
179+
print(log_lines)
169180
self.printer.add(heading + "=" * max(0, width - len(heading)))
170181
for line in log_lines[-max_log_lines:]:
171182
self.printer.add(line)

tests/test_app.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""
2+
Tests for TerminalApp
3+
"""
4+
5+
# Standard
6+
from contextlib import contextmanager
7+
from unittest import mock
8+
import logging
9+
import os
10+
import tempfile
11+
12+
from tests.conftest import ResettableStringIO
13+
14+
# Local
15+
from scriptit import RefreshPrinter, TerminalApp
16+
17+
18+
@contextmanager
19+
def reset_logging():
20+
"""Fixture to reset global changes to the logging module
21+
22+
NOTE: This uses a contextmanager instead of a fixture because the pytest log
23+
capture system takes place _after_ a fixture runs and therefore the
24+
tests end up with multiple handlers.
25+
"""
26+
new_root = logging.RootLogger(logging.root.level)
27+
new_manager = logging.Manager(new_root)
28+
new_root.manager = new_manager
29+
with mock.patch("logging.root", new_root):
30+
with mock.patch.object(logging.Logger, "manager", new_manager):
31+
with mock.patch.object(logging.Logger, "root", new_root):
32+
logging.basicConfig()
33+
log = new_root.getChild("TEST")
34+
yield log
35+
36+
37+
def test_app_basic():
38+
"""Test that the basic execution of the app works as expected"""
39+
with reset_logging() as log:
40+
# Set up the app that will capture the logging
41+
stream = ResettableStringIO()
42+
app = TerminalApp(write_stream=stream)
43+
44+
# Log and make sure the content went to the stream
45+
log.warning("hello")
46+
lines = stream.getvalue().split("\n")
47+
assert len(lines) == 4
48+
assert lines[0].startswith(TerminalApp.CONSOLE_START)
49+
assert "hello" in lines[1]
50+
assert set([ch for ch in lines[2].strip()]) == {"="}
51+
assert not lines[3].strip()
52+
stream.reset()
53+
54+
# Add some content and refresh. This will include the logs and the added
55+
# content lines
56+
app.add("Line one")
57+
app.add("Line two")
58+
app.refresh()
59+
lines = stream.getvalue().split("\n")
60+
assert len(lines) == 7
61+
assert lines[0].count(RefreshPrinter.UP_LINE) == 4
62+
assert lines[1].startswith(TerminalApp.CONSOLE_START)
63+
assert "hello" in lines[2]
64+
assert set([ch for ch in lines[3].strip()]) == {"="}
65+
assert lines[4].strip() == "Line one"
66+
assert lines[5].strip() == "Line two"
67+
assert not lines[6].strip()
68+
69+
70+
def test_app_log_file():
71+
"""Test that logging to a file works alongside the app"""
72+
with tempfile.TemporaryDirectory() as workdir, reset_logging() as log:
73+
log_file = os.path.join(workdir, "test.log")
74+
stream = ResettableStringIO()
75+
TerminalApp(write_stream=stream, log_file=log_file)
76+
77+
# Log and make sure the content went to the stream and the file
78+
log.warning("hello")
79+
lines = stream.getvalue().split("\n")
80+
assert len(lines) == 4
81+
with open(log_file, "r") as handle:
82+
log_file_lines = list(handle.readlines())
83+
assert len(log_file_lines) == 1
84+
stream.reset()
85+
86+
# Log again and make sure there are now two log lines in the file
87+
log.warning("world")
88+
lines = stream.getvalue().split("\n")
89+
assert len(lines) == 6 # Clear + extra log line
90+
with open(log_file, "r") as handle:
91+
log_file_lines = list(handle.readlines())
92+
assert len(log_file_lines) == 2
93+
94+
95+
def test_app_preserve_logs():
96+
"""Test that preserving existing loggers works as expected"""
97+
with reset_logging() as log:
98+
stream = ResettableStringIO()
99+
logger_stream = ResettableStringIO()
100+
logging.root.addHandler(logging.StreamHandler(logger_stream))
101+
TerminalApp(write_stream=stream, preserve_log_handlers=True)
102+
log.warning("hello")
103+
logged_lines = logger_stream.getvalue().split("\n")
104+
assert len(logged_lines) == 2
105+
assert "hello" in logged_lines[0]
106+
assert not logged_lines[1].strip()
107+
108+
109+
def test_app_pad_log_console():
110+
"""Test that the log console can be padded to a fixed size"""
111+
with reset_logging():
112+
stream = ResettableStringIO()
113+
TerminalApp(
114+
write_stream=stream,
115+
pad_log_console=True,
116+
log_console_size=5,
117+
)
118+
log = logging.getLogger("NEW")
119+
log.warning("watch out")
120+
lines = stream.getvalue().split("\n")
121+
assert len(lines) == 6 # Padded to 5 plus last newline
122+
123+
124+
def test_app_logging_post_config():
125+
"""Test that logging can be configured after the app initializes and log
126+
placeholders don't cause any problems
127+
"""
128+
with reset_logging():
129+
stream = ResettableStringIO()
130+
TerminalApp(write_stream=stream)
131+
logging.basicConfig(level=logging.INFO, force=True)
132+
log = logging.getLogger("foo.bar.baz")
133+
log.info("hello there")
134+
lines = stream.getvalue().split("\n")
135+
assert len(lines) == 4 # Header, line, footer, newline
136+
137+
138+
def test_app_logging_direct_add_handler():
139+
"""Make sure that addHandler called on a logger will get wrapped"""
140+
with reset_logging():
141+
stream = ResettableStringIO()
142+
TerminalApp(write_stream=stream)
143+
log = logging.getLogger("test")
144+
log.addHandler(logging.StreamHandler())
145+
log.setLevel(logging.DEBUG)
146+
log.propagate = False
147+
log.debug("watch out")
148+
lines = stream.getvalue().split("\n")
149+
assert len(lines) == 4 # Header, line, footer, newline
150+
151+
152+
def test_app_logging_placeholder():
153+
"""Make sure that logging placeholders can be safely reconfigured"""
154+
with reset_logging():
155+
log = logging.getLogger("foo.bar.baz")
156+
stream = ResettableStringIO()
157+
TerminalApp(write_stream=stream)
158+
log.warning("test")
159+
lines = stream.getvalue().split("\n")
160+
assert len(lines) == 4 # Header, line, footer, newline
161+
162+
163+
def test_app_wrap_long_log_lines():
164+
"""Make sure long lines get wrapped if they exceed the term width"""
165+
term_size_mock = mock.MagicMock()
166+
term_width = 20
167+
term_size_mock.columns = term_width
168+
term_size_mock.lines = 150
169+
with reset_logging() as log, mock.patch(
170+
"shutil.get_terminal_size", return_value=term_size_mock
171+
):
172+
stream = ResettableStringIO()
173+
TerminalApp(write_stream=stream)
174+
msg = "*" * int(term_width * 2.1)
175+
log.warning(msg)
176+
lines = stream.getvalue().split("\n")
177+
assert len(lines) == 6

0 commit comments

Comments
 (0)