MBSConsoleHandler: show status of ongoing repository downloads

When downloading files from Koji to make a local repository, display
a temporary status of which files are being displayed to the console
appended after any log messages. Updates are done by erasing the status
that was written, adding a log message, then writing the status again.
This commit is contained in:
Owen W. Taylor
2020-11-09 13:12:01 -05:00
committed by breilly
parent f5100609aa
commit 646b0590ee
6 changed files with 407 additions and 40 deletions

View File

@@ -5,6 +5,7 @@ import io
import logging
import os
from os import path
import re
import pytest
import shutil
import tempfile
@@ -119,6 +120,125 @@ class TestLogger:
assert log2.path(db_session, build) == "/some/path/build-nginx-1-2.log"
class FakeTerminal(object):
"""
Just enough terminal to allow meaningfully testing the terminal fanciness
in MBSConsoleHandler
"""
def __init__(self):
self.serializer = FakeTerminalSerializer(self)
self.reset()
self.columns = 80
self.rows = 24
def get_size(self):
return os.terminal_size((self.columns, self.rows))
def isatty(self):
return True
def fileno(self):
return 42
def flush(self):
pass
def write(self, raw):
for m in re.finditer(r'\n|\033\[[0-9;]*[A-Za-z]|\033|[^\033\n]*', raw):
piece = m.group(0)
if piece == '\n':
self._next_row()
elif piece == '\033[0m':
self.cur_attr = "x"
elif piece == '\033[1m':
self.cur_attr = self.cur_attr.upper()
elif piece == '\033[32m':
self.cur_attr = 'G' if self.cur_attr.isupper() else 'g'
elif piece == '\033[91m':
self.cur_attr = 'R' if self.cur_attr.isupper() else 'r'
elif piece == '\033[0G':
self.cursor_column = 0
elif piece == '\033[1A':
self.cursor_column = 0
self.cursor_row = max(self.cursor_row - 1, 0)
elif piece == '\033[2K':
self.text[self.cursor_row] = ' ' * self.cursor_column
self.attrs[self.cursor_row] = 'x' * self.cursor_column
elif piece.startswith('\033['):
raise RuntimeError("Unhandled CSI sequence: %r", piece)
else:
pos = 0
while len(piece) - pos > self.columns - self.cursor_column:
to_insert = self.columns - self.cursor_column
self._insert(piece[pos:pos + to_insert])
pos += to_insert
self._next_row()
self._insert(piece[pos:])
def _next_row(self):
self.cursor_row += 1
if self.cursor_row == len(self.text):
self.text.append('')
self.attrs.append('')
self.cursor_column = 0
def _insert(self, string):
text = self.text[self.cursor_row]
self.text[self.cursor_row] = (text[0:self.cursor_column]
+ string
+ text[self.cursor_column + len(string):])
attrs = self.attrs[self.cursor_row]
self.attrs[self.cursor_row] = (attrs[0:self.cursor_column]
+ self.cur_attr * len(string)
+ attrs[self.cursor_column + len(string):])
self.cursor_column += len(string)
def reset(self):
self.text = ['']
self.attrs = ['']
self.cur_attr = 'x'
self.cursor_row = 0
self.cursor_column = 0
def serialize(self):
return self.serializer.serialize()
class FakeTerminalSerializer(object):
"""Serializes the terminal contents with <R></R> tags to represent attributes"""
def __init__(self, terminal):
self.terminal = terminal
def serialize(self):
self.result = io.StringIO()
self.last_attr = 'x'
for row in range(0, len(self.terminal.text)):
text = self.terminal.text[row]
attrs = self.terminal.attrs[row]
for col in range(0, len(text)):
self.set_attr(attrs[col])
self.result.write(text[col])
self.set_attr('x')
self.result.write('\n')
value = self.result.getvalue()
self.result = None
return value
def set_attr(self, attr):
if attr != self.last_attr:
if self.last_attr != 'x':
self.result.write('</' + self.last_attr + '>')
if attr != 'x':
self.result.write('<' + attr + '>')
self.last_attr = attr
class TestConsoleHandler:
def terminal(test_method):
test_method.terminal = True
@@ -132,15 +252,18 @@ class TestConsoleHandler:
return decorate
def setup_method(self, test_method):
self.stream = io.StringIO()
if getattr(test_method, 'terminal', False):
self.stream = FakeTerminal()
self.get_terminal_size_patcher = patch("os.get_terminal_size")
mock_get_terminal_size = self.get_terminal_size_patcher.start()
mock_get_terminal_size.return_value = os.terminal_size((80, 24))
self.stream.isatty = lambda: True
self.stream.fileno = lambda: 42
def get_terminal_size(fileno):
return self.stream.get_size()
mock_get_terminal_size.side_effect = get_terminal_size
else:
self.stream = io.StringIO()
self.handler = MBSConsoleHandler(stream=self.stream)
self.handler.level = getattr(test_method, 'level', logging.INFO)
@@ -153,6 +276,14 @@ class TestConsoleHandler:
if getattr(test_method, 'terminal', False):
self.get_terminal_size_patcher.stop()
def current(self):
if isinstance(self.stream, FakeTerminal):
val = self.stream.serialize()
else:
val = self.stream.getvalue()
return val.rstrip() + '\n'
def log_messages(self):
log.debug("Debug")
log.info("Info")
@@ -163,35 +294,35 @@ class TestConsoleHandler:
def test_console_basic(self):
self.log_messages()
value = self.stream.getvalue()
assert "Debug" not in value
assert "Info" not in value
assert "Console" in value
assert "\nWARNING - Warning" in value
assert "\nERROR - Error" in value
current = self.current()
assert "Debug" not in current
assert "Info" not in current
assert "Console" in current
assert "\nWARNING - Warning" in current
assert "\nERROR - Error" in current
@terminal
def test_console_terminal(self):
self.log_messages()
value = self.stream.getvalue()
assert "Debug" not in value
assert "Info" not in value
assert "Console" in value
assert "\n\x1b[1m\x1b[91mWARNING\x1b[0m - Warning" in value
assert "\n\x1b[1m\x1b[91mERROR\x1b[0m - Error" in value
current = self.current()
assert "Debug" not in current
assert "Info" not in current
assert "Console" in current
assert "<R>WARNING</R> - Warning" in current
assert "<R>ERROR</R> - Error" in current
@level(logging.DEBUG)
@terminal
def test_console_debug(self):
self.log_messages()
value = self.stream.getvalue()
assert "MainThread - MBS - DEBUG - Debug" in value
assert "MainThread - MBS - INFO - Info" in value
assert "MainThread - MBS - INFO - Console" in value
assert "MainThread - MBS - WARNING - Warning" in value
assert "MainThread - MBS - ERROR - Error" in value
current = self.current()
assert "MainThread - MBS - DEBUG - Debug" in current
assert "MainThread - MBS - INFO - Info" in current
assert "MainThread - MBS - INFO - Console" in current
assert "MainThread - MBS - WARNING - Warning" in current
assert "MainThread - MBS - ERROR - Error" in current
@terminal
def test_console_long_running(self):
@@ -209,9 +340,7 @@ class TestConsoleHandler:
log.long_running_end("Blabbing")
log.long_running_end("Frobulating")
print(self.stream.getvalue())
assert self.stream.getvalue() == textwrap.dedent("""\
assert self.current() == textwrap.dedent("""\
Frobulating ... done
---
Frobulating ...
@@ -222,3 +351,76 @@ class TestConsoleHandler:
Blabbing ... done
Frobulating ... done
""")
@terminal
def test_console_local_repo(self):
log.local_repo_start("module-testmodule-build")
assert self.current() == textwrap.dedent("""\
<X>module-testmodule-build</X>: Making local repository for Koji tag
------------------------------
<X>module-testmodule-build</X>:
""")
log.local_repo_start_downloads("module-testmodule-build", 42, "/tmp/download-dir")
log.local_repo_start_download("module-testmodule-build",
"https://ftp.example.com/libsomething-1.2.3-1.x86_64.rpm")
log.local_repo_start_download("module-testmodule-build",
"https://ftp.example.com/libother-1.2.3-1.x86_64.rpm")
log.local_repo_done_download("module-testmodule-build",
"https://ftp.example.com/libother-1.2.3-1.x86_64.rpm")
assert self.current() == textwrap.dedent("""\
<X>module-testmodule-build</X>: Making local repository for Koji tag
------------------------------
<X>module-testmodule-build</X>: Downloading packages 1/42
libsomething-1.2.3-1.x86_64.rpm
""")
log.local_repo_done_download("module-testmodule-build",
"https://ftp.example.com/libsomething-1.2.3-1.x86_64.rpm")
log.local_repo_done("module-testmodule-build", "downloaded everything")
assert self.current() == textwrap.dedent("""\
<X>module-testmodule-build</X>: Making local repository for Koji tag
<X>module-testmodule-build</X>: <G>downloaded everything</G>
""")
@terminal
def test_console_local_repo_wrap(self):
log.local_repo_start("module-testmodule-build")
long_url = "https://ftp.example.com/lib" + (80 * "z") + "-1.2.3-1.x86_64.rpm"
log.local_repo_start_downloads("module-testmodule-build", 42, "/tmp/download-dir")
log.local_repo_start_download("module-testmodule-build", long_url)
assert self.current() == textwrap.dedent("""\
<X>module-testmodule-build</X>: Making local repository for Koji tag
------------------------------
<X>module-testmodule-build</X>: Downloading packages 0/42
libzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
zzzzzzz-1.2.3-1.x86_64.rpm
""")
log.local_repo_done_download("module-testmodule-build", long_url)
log.local_repo_done("module-testmodule-build", "downloaded everything")
assert self.current() == textwrap.dedent("""\
<X>module-testmodule-build</X>: Making local repository for Koji tag
<X>module-testmodule-build</X>: <G>downloaded everything</G>
""")
@terminal
def test_console_partial_line_erase(self):
self.handler.status_stream.write("Foo\nBar")
self.handler.status_stream.erase()
self.handler.status_stream.write("Baz")
assert self.current() == "Baz\n"
@terminal
def test_console_resize(self):
self.stream.columns = 20
self.handler.resize()
self.stream.write("Foo\n")
self.handler.status_stream.write(30 * "x")
self.handler.status_stream.erase()
self.handler.status_stream.write("Baz")
assert self.current() == "Foo\nBaz\n"