Additional tests and docstrings.

This commit is contained in:
Dan Helfman
2026-05-30 11:27:59 -07:00
parent ed955c2a73
commit 3a4bdfb3c5
5 changed files with 168 additions and 12 deletions
+25
View File
@@ -9,6 +9,11 @@ import borgmatic.logger
class Rich_color_formatter(logging.Formatter):
'''
A Python logging formatter that formats log records with Rich-compatible color markup according
to their levels.
'''
def __init__(self, *args, **kwargs):
self.prefix = None
super().__init__(
@@ -19,6 +24,10 @@ class Rich_color_formatter(logging.Formatter):
)
def format(self, record):
'''
Given a log record, format it with Rich-compatibe color markup correponding to its log
level.
'''
borgmatic.logger.add_custom_log_levels()
color = {
@@ -35,12 +44,23 @@ class Rich_color_formatter(logging.Formatter):
class Browse_log_handler(logging.Handler):
'''
A Python log handler that writes any log records to a logging widget.
'''
def __init__(self, logs_widget):
'''
Given a logs widget, save it for use below.
'''
self.logs_widget = logs_widget
super().__init__()
def emit(self, record):
'''
Given a log record, format it and log it to the logs widgets. This works whether or not the
logging is happening in the main thread.
'''
message = self.format(record)
try:
@@ -72,6 +92,11 @@ def log_to_widget(logs_widget):
class Logs(textual.widgets.RichLog):
'''
A widget for viewing borgmatic logs in realtime. The log level is determined by borgmatic's
current verbosity level.
'''
def __init__(self):
super().__init__(markup=True, id='logs', classes='panel')
self.border_title = '🪵 logs'
@@ -200,7 +200,9 @@ def test_directory_list_on_mount_with_non_root_directory_adds_archive_paths():
).and_return(flexmock())
flexmock(module).should_receive('add_archive_paths').once()
directory_list = module.Directory_list(
config=flexmock(), repository=flexmock(), archive_name='archive',
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
@@ -216,7 +218,9 @@ def test_on_archive_path_loaded_with_loading_done_signal_removes_loading_indicat
).and_return(flexmock())
flexmock(module).should_receive('add_archive_paths').never()
directory_list = module.Directory_list(
config=flexmock(), repository=flexmock(), archive_name='archive',
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
@@ -233,7 +237,9 @@ def test_on_archive_path_loaded_with_path_loaded_signal_adds_archive_path():
).and_return(flexmock())
flexmock(module).should_receive('add_archive_paths').once()
directory_list = module.Directory_list(
config=flexmock(), repository=flexmock(), archive_name='archive',
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
@@ -248,7 +254,9 @@ def test_directory_list_on_option_list_option_highlighted_with_highlighted_none_
'app'
).and_return(flexmock())
directory_list = module.Directory_list(
config=flexmock(), repository=flexmock(), archive_name='archive',
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
@@ -265,7 +273,9 @@ def test_directory_list_on_option_list_option_highlighted_with_highlighted_zero_
'app'
).and_return(flexmock())
directory_list = module.Directory_list(
config=flexmock(), repository=flexmock(), archive_name='archive',
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
@@ -283,7 +293,9 @@ def test_directory_list_on_option_list_option_highlighted_with_highlighted_non_z
'app'
).and_return(flexmock())
directory_list = module.Directory_list(
config=flexmock(), repository=flexmock(), archive_name='archive',
config=flexmock(),
repository=flexmock(),
archive_name='archive',
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
path_components=('etc',),
)
@@ -8,9 +8,7 @@ import textual.widgets
def test_update_inline_loading_indicator_with_option_list_adds_a_dot():
widget = textual.widgets.OptionList()
widget.add_option(
textual.widgets.option_list.Option('HOLD.', id='loading-indicator')
)
widget.add_option(textual.widgets.option_list.Option('HOLD.', id='loading-indicator'))
module.update_inline_loading_indicator(widget)
@@ -20,9 +18,7 @@ def test_update_inline_loading_indicator_with_option_list_adds_a_dot():
def test_update_inline_loading_indicator_with_option_list_wraps_dots_beyond_three():
widget = textual.widgets.OptionList()
widget.add_option(
textual.widgets.option_list.Option('HOLD...', id='loading-indicator')
)
widget.add_option(textual.widgets.option_list.Option('HOLD...', id='loading-indicator'))
module.update_inline_loading_indicator(widget)
@@ -0,0 +1,36 @@
import contextlib
from borgmatic.actions.browse import logs as module
from flexmock import flexmock
def test_log_to_widget_adds_our_handler_and_removes_default_handler():
default_handler = module.borgmatic.logger.Multi_stream_handler({})
root_logger = module.logging.getLogger()
root_logger.addHandler(default_handler)
browse_log_handler = None
try:
module.log_to_widget(flexmock())
with contextlib.suppress(StopIteration):
browse_log_handler = next(
handler for handler in root_logger.handlers
if isinstance(handler, module.Browse_log_handler)
)
assert browse_log_handler
finally:
if browse_log_handler:
root_logger.removeHandler(browse_log_handler)
assert not any(
handler for handler in root_logger.handlers
if isinstance(handler, module.borgmatic.logger.Multi_stream_handler)
)
def test_logs_does_not_raise():
module.Logs()
+87
View File
@@ -0,0 +1,87 @@
from borgmatic.actions.browse import logs as module
from flexmock import flexmock
def test_rich_color_formatter_format_colors_log_record_based_on_level():
formatted = module.Rich_color_formatter().format(
module.logging.makeLogRecord(
dict(
levelno=module.logging.ERROR,
levelname='ERROR',
msg='oh no',
)
),
)
assert formatted == '[bright_red]oh no[/bright_red]'
def test_rich_color_formatter_format_includes_prefix():
formatter = module.Rich_color_formatter()
formatter.prefix = 'sup'
formatted = formatter.format(
module.logging.makeLogRecord(
dict(
levelno=module.logging.ERROR,
levelname='ERROR',
msg='oh no',
),
),
)
assert formatted == '[bright_red]sup: oh no[/bright_red]'
def test_browse_log_handler_emit_from_worker_thread_calls_write_in_main_thread():
flexmock(module.textual.worker).should_receive('get_current_worker')
logs_widget = flexmock(write=lambda message: None, app=flexmock())
flexmock(logs_widget.app).should_receive('call_from_thread').with_args(
logs_widget.write, 'hi'
).once()
module.Browse_log_handler(logs_widget).emit(
module.logging.makeLogRecord(
dict(
levelno=module.logging.DEBUG,
levelname='DEBUG',
msg='hi',
),
),
)
def test_browse_log_handler_emit_from_main_thread_calls_write_directly():
flexmock(module.textual.worker).should_receive('get_current_worker').and_raise(RuntimeError)
logs_widget = flexmock(write=lambda message: None, app=flexmock())
flexmock(logs_widget.app).should_receive('call_from_thread').never()
flexmock(logs_widget).should_receive('write').with_args('hi').once()
module.Browse_log_handler(logs_widget).emit(
module.logging.makeLogRecord(
dict(
levelno=module.logging.DEBUG,
levelname='DEBUG',
msg='hi',
),
),
)
def test_browse_log_handler_emit_from_main_thread_with_no_active_app_does_not_raise():
flexmock(module.textual.worker).should_receive('get_current_worker').and_raise(RuntimeError)
logs_widget = flexmock(write=lambda message: None, app=flexmock())
flexmock(logs_widget.app).should_receive('call_from_thread').never()
flexmock(logs_widget).should_receive('write').with_args('hi').and_raise(
module.textual._context.NoActiveAppError
)
module.Browse_log_handler(logs_widget).emit(
module.logging.makeLogRecord(
dict(
levelno=module.logging.DEBUG,
levelname='DEBUG',
msg='hi',
),
),
)