More browse tests.

This commit is contained in:
Dan Helfman
2026-05-24 19:22:50 -07:00
parent 60201541e0
commit 4b8a6d1a34
8 changed files with 219 additions and 26 deletions
+10 -2
View File
@@ -8,6 +8,14 @@ import borgmatic.actions.browse.paths
def make_next_panel(focused_panel, option_id):
'''
Given a focused panel widget and the selected option ID, return the next panel corresponding to
that selection. This is the mechanism by which the user can successively drill down from
configuration file to repository to archive to root directory to non-root directory or file.
If the particular option ID on the focused panel doesn't have a supported next panel, then
return None.
'''
if isinstance(focused_panel, borgmatic.actions.browse.panels.Configuration_files_list):
return borgmatic.actions.browse.panels.Repositories_list(
config=focused_panel.configs[option_id]
@@ -101,7 +109,7 @@ class Carousel(textual.containers.Horizontal):
next_panel = make_next_panel(self.focused_panel, option_id)
if next_panel is None:
self.notify('Cannot load this content', severity='warning')
self.notify('Cannot display this content', severity='warning')
return
self.panels.append(next_panel)
@@ -125,7 +133,7 @@ class Carousel(textual.containers.Horizontal):
An option has been selected, so advance to the next panel—unless the option selected is
"..", in which case go to the previous panel.
'''
if event.option_list != self.focused_panel or event.option_id == 'loading-indicator':
if event.option_list != self.focused_panel or event.option_id == 'loading-indicator': # pragma: no cover
return
if event.option_id == '..':
-2
View File
@@ -61,8 +61,6 @@ def log_to_widget(logs_widget):
logger.setLevel(min(handler.level for handler in logger.handlers))
logger.addHandler(handler)
# Remove the console log handler so it doesn't try to log all over our UI; we have our own
# log handler for surfacing logs within the UI.
with contextlib.suppress(StopIteration):
console_handler = next(
handler
+8 -5
View File
@@ -59,9 +59,10 @@ def add_archive_files(
)
for path_type, file_path, link_target in file_type_paths:
pieces = (borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get(path_type, ''), file_path) + (
('', link_target) if link_target else ()
)
pieces = (
borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get(path_type, ''),
file_path,
) + (('', link_target) if link_target else ())
highlighted_option = directory_list.highlighted_option
sorted_options = sorted(
[
@@ -91,7 +92,9 @@ def load_file_preview(browse_app, file_preview, config, repository, archive_name
browse_app.call_from_thread(file_preview.clear)
if file_content is None:
browse_app.call_from_thread(file_preview.write, 'Cannot load a preview for this file')
browse_app.call_from_thread(file_preview.write, 'Cannot display a preview for this file')
else:
syntax_lexer = rich.syntax.Syntax.guess_lexer(file_path, file_content)
browse_app.call_from_thread(file_preview.write, rich.syntax.Syntax(file_content, syntax_lexer))
browse_app.call_from_thread(
file_preview.write, rich.syntax.Syntax(file_content, syntax_lexer)
)
+1 -1
View File
@@ -47,7 +47,7 @@ namespaces = false
[tool.pytest.ini_options]
testpaths = "tests"
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end --timeout=120"
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end --timeout=120 --asyncio-mode=auto"
[tool.ruff]
line-length = 100
+1
View File
@@ -27,5 +27,6 @@ requests-oauthlib
rich
rpds-py
ruamel-yaml>0.15.0
textual
typing-extensions
urllib3
+9 -4
View File
@@ -13,13 +13,16 @@ idna==3.16 # via requests, -r test_requirements.in
iniconfig==2.3.0 # via pytest, -r test_requirements.in
jsonschema==4.26.0 # via -r test_requirements.in
jsonschema-specifications==2025.9.1 # via jsonschema, -r test_requirements.in
linkify-it-py==2.1.0 # via markdown-it-py
markdown==3.10.2 # via apprise, -r test_requirements.in
markdown-it-py==4.2.0 # via rich
markdown-it-py==4.2.0 # via mdit-py-plugins, rich, textual
mdit-py-plugins==0.6.1 # via textual
mdurl==0.1.2 # via markdown-it-py
oauthlib==3.3.1 # via requests-oauthlib, -r test_requirements.in
packaging==26.2 # via pytest, -r test_requirements.in
platformdirs==4.9.6 # via textual
pluggy==1.6.0 # via pytest, pytest-cov, -r test_requirements.in
pygments==2.20.0 # via pytest, rich, -r test_requirements.in
pygments==2.20.0 # via pytest, rich, textual, -r test_requirements.in
pytest==9.0.3 # via pytest-asyncio, pytest-cov, pytest-timeout, -r test_requirements.in
pytest-asyncio==1.3.0 # via -r test_requirements.in
pytest-cov==7.1.0 # via -r test_requirements.in
@@ -28,8 +31,10 @@ pyyaml>5.0.0
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r test_requirements.in
requests==2.34.2 # via apprise, requests-oauthlib, -r test_requirements.in
requests-oauthlib==2.0.0 # via apprise, -r test_requirements.in
rich==15.0.0 # via -r test_requirements.in
rich==15.0.0 # via textual, -r test_requirements.in
rpds-py==0.30.0 # via jsonschema, referencing, -r test_requirements.in
ruamel-yaml>0.15.0
typing-extensions==4.15.0 # via -r test_requirements.in
textual==8.2.7 # via -r test_requirements.in
typing-extensions==4.15.0 # via textual, -r test_requirements.in
uc-micro-py==2.0.0 # via linkify-it-py
urllib3==2.7.0 # via requests, -r test_requirements.in
@@ -1,5 +1,3 @@
import asyncio
import borgmatic.actions.browse.app
import borgmatic.actions.browse.panels
@@ -7,10 +5,6 @@ import pytest
from flexmock import flexmock
pytestmark = pytest.mark.asyncio(loop_scope='module')
loop: asyncio.AbstractEventLoop
async def test_browse_app_with_multiple_configs_uses_configuration_files_list():
app = borgmatic.actions.browse.app.Browse_app(
configs={
@@ -1,19 +1,182 @@
import asyncio
import borgmatic.actions.browse.app
import borgmatic.actions.browse.carousel
import borgmatic.actions.browse.loading
import borgmatic.actions.browse.logs
import borgmatic.actions.browse.panels
import borgmatic.actions.browse.workers
import pytest
import pytest_asyncio
from flexmock import flexmock
import textual.widgets.option_list
from borgmatic.actions.browse import carousel as module
pytestmark = pytest.mark.asyncio(loop_scope='module')
loop: asyncio.AbstractEventLoop
def test_make_next_panel_with_configuration_files_list_returns_repositories_list():
configs = {'test.yaml': {'repositories': [{'path': 'test.borg'}]}}
repositories_list = module.make_next_panel(
focused_panel=borgmatic.actions.browse.panels.Configuration_files_list(configs),
option_id='test.yaml',
)
assert isinstance(repositories_list, borgmatic.actions.browse.panels.Repositories_list)
assert repositories_list.config == configs['test.yaml']
def test_make_next_panel_with_repositories_list_returns_archives_list():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(borgmatic.actions.browse.workers).should_receive('add_repository_archives')
flexmock(borgmatic.actions.browse.panels.Archives_list).should_receive('app').and_return(
flexmock()
)
archives_list = module.make_next_panel(
focused_panel=borgmatic.actions.browse.panels.Repositories_list(config),
option_id=0,
)
assert isinstance(archives_list, borgmatic.actions.browse.panels.Archives_list)
assert archives_list.config == config
assert archives_list.repository == config['repositories'][0]
def test_make_next_panel_with_archives_list_returns_directory_list():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(borgmatic.actions.browse.workers).should_receive('add_repository_archives')
flexmock(borgmatic.actions.browse.workers).should_receive('add_archive_files')
flexmock(borgmatic.actions.browse.panels.Archives_list).should_receive('app').and_return(
flexmock()
)
flexmock(borgmatic.actions.browse.panels.Directory_list).should_receive('app').and_return(
flexmock()
)
directory_list = module.make_next_panel(
focused_panel=borgmatic.actions.browse.panels.Archives_list(
config, config['repositories'][0]
),
option_id='archive',
)
assert isinstance(directory_list, borgmatic.actions.browse.panels.Directory_list)
assert directory_list.config == config
assert directory_list.repository == config['repositories'][0]
def test_make_next_panel_with_root_directory_list_and_selected_directory_option_returns_new_directory_list():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(borgmatic.actions.browse.workers).should_receive('add_archive_files')
flexmock(borgmatic.actions.browse.panels.Directory_list).should_receive('app').and_return(
flexmock()
)
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive'
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📁 etc'))
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='etc')
assert isinstance(directory_list, borgmatic.actions.browse.panels.Directory_list)
assert directory_list.config == config
assert directory_list.repository == config['repositories'][0]
assert directory_list.archive_name == 'archive'
assert directory_list.path_components == ('etc',)
def test_make_next_panel_with_non_root_directory_list_and_selected_directory_option_returns_new_directory_list():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(borgmatic.actions.browse.workers).should_receive('add_archive_files')
flexmock(borgmatic.actions.browse.panels.Directory_list).should_receive('app').and_return(
flexmock()
)
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive', path_components=('etc',),
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📁 borgmatic'))
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='borgmatic')
assert isinstance(directory_list, borgmatic.actions.browse.panels.Directory_list)
assert directory_list.config == config
assert directory_list.repository == config['repositories'][0]
assert directory_list.archive_name == 'archive'
assert directory_list.path_components == ('etc', 'borgmatic')
def test_make_next_panel_with_root_directory_list_and_selected_file_option_returns_new_file_preview():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(borgmatic.actions.browse.workers).should_receive('add_archive_files')
flexmock(borgmatic.actions.browse.workers).should_receive('load_file_preview')
flexmock(borgmatic.actions.browse.panels.Directory_list).should_receive('app').and_return(
flexmock()
)
flexmock(borgmatic.actions.browse.panels.File_preview).should_receive('app').and_return(
flexmock()
)
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive'
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📄 config.yaml'))
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml')
assert isinstance(directory_list, borgmatic.actions.browse.panels.File_preview)
assert directory_list.config == config
assert directory_list.repository == config['repositories'][0]
assert directory_list.archive_name == 'archive'
assert directory_list.file_path == 'config.yaml'
def test_make_next_panel_with_non_root_directory_list_and_selected_file_option_returns_new_file_preview():
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(borgmatic.actions.browse.workers).should_receive('add_archive_files')
flexmock(borgmatic.actions.browse.workers).should_receive('load_file_preview')
flexmock(borgmatic.actions.browse.panels.Directory_list).should_receive('app').and_return(
flexmock()
)
flexmock(borgmatic.actions.browse.panels.File_preview).should_receive('app').and_return(
flexmock()
)
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive', path_components=('etc', 'borgmatic'),
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📄 config.yaml'))
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml')
assert isinstance(directory_list, borgmatic.actions.browse.panels.File_preview)
assert directory_list.config == config
assert directory_list.repository == config['repositories'][0]
assert directory_list.archive_name == 'archive'
assert directory_list.file_path == 'etc/borgmatic/config.yaml'
def test_make_next_panel_with_unsupported_focused_panel_returns_none():
assert module.make_next_panel(focused_panel=flexmock(), option_id='hmmm') is None
@pytest.mark.parametrize('icon', ('🔗', '🚰', '🐙'))
def test_make_next_panel_with_directory_list_and_unsupported_selected_option_returns_none(icon):
config = {'repositories': [{'path': 'test.borg'}]}
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
flexmock(borgmatic.actions.browse.workers).should_receive('add_archive_files')
flexmock(borgmatic.actions.browse.panels.Directory_list).should_receive('app').and_return(
flexmock()
)
focused_panel = borgmatic.actions.browse.panels.Directory_list(
config, config['repositories'][0], 'archive'
)
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt=f'{icon} config.yaml'))
assert module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml') is None
async def test_carousel_previous_action_with_multiple_configs_does_not_raise():
@@ -41,7 +204,7 @@ async def test_carousel_previous_action_with_one_config_does_not_raise():
await pilot.press('left')
async def test_carousel_next_action_with_multiple_configs_advances_panels():
async def test_carousel_next_action_with_multiple_configs_advances_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
@@ -67,7 +230,7 @@ async def test_carousel_next_action_with_multiple_configs_advances_panels():
assert app.focused == carousel.panels[1]
async def test_carousel_next_action_with_one_config_advances_to_next_panel():
async def test_carousel_next_action_with_one_config_advances_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
@@ -91,6 +254,27 @@ async def test_carousel_next_action_with_one_config_advances_to_next_panel():
assert app.focused == carousel.panels[1]
async def test_carousel_next_action_with_no_next_panel_does_not_advance():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
}
)
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
flexmock(borgmatic.actions.browse.workers).should_receive('add_repository_archives')
flexmock(module).should_receive('make_next_panel')
async with app.run_test() as pilot:
await pilot.press('enter')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 1
assert isinstance(carousel.panels[0], borgmatic.actions.browse.panels.Repositories_list)
assert carousel.panels[0].styles.display == 'block'
assert app.focused == carousel.panels[0]
async def test_carousel_next_action_and_previous_action_returns_to_original_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={