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): 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): if isinstance(focused_panel, borgmatic.actions.browse.panels.Configuration_files_list):
return borgmatic.actions.browse.panels.Repositories_list( return borgmatic.actions.browse.panels.Repositories_list(
config=focused_panel.configs[option_id] 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) next_panel = make_next_panel(self.focused_panel, option_id)
if next_panel is None: if next_panel is None:
self.notify('Cannot load this content', severity='warning') self.notify('Cannot display this content', severity='warning')
return return
self.panels.append(next_panel) 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 An option has been selected, so advance to the next panel—unless the option selected is
"..", in which case go to the previous panel. "..", 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 return
if event.option_id == '..': 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.setLevel(min(handler.level for handler in logger.handlers))
logger.addHandler(handler) 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): with contextlib.suppress(StopIteration):
console_handler = next( console_handler = next(
handler handler
+8 -5
View File
@@ -59,9 +59,10 @@ def add_archive_files(
) )
for path_type, file_path, link_target in file_type_paths: for path_type, file_path, link_target in file_type_paths:
pieces = (borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get(path_type, ''), file_path) + ( pieces = (
('', link_target) if link_target else () borgmatic.actions.browse.paths.PATH_TYPE_ICONS.get(path_type, ''),
) file_path,
) + (('', link_target) if link_target else ())
highlighted_option = directory_list.highlighted_option highlighted_option = directory_list.highlighted_option
sorted_options = sorted( 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) browse_app.call_from_thread(file_preview.clear)
if file_content is None: 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: else:
syntax_lexer = rich.syntax.Syntax.guess_lexer(file_path, file_content) 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] [tool.pytest.ini_options]
testpaths = "tests" 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] [tool.ruff]
line-length = 100 line-length = 100
+1
View File
@@ -27,5 +27,6 @@ requests-oauthlib
rich rich
rpds-py rpds-py
ruamel-yaml>0.15.0 ruamel-yaml>0.15.0
textual
typing-extensions typing-extensions
urllib3 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 iniconfig==2.3.0 # via pytest, -r test_requirements.in
jsonschema==4.26.0 # via -r test_requirements.in jsonschema==4.26.0 # via -r test_requirements.in
jsonschema-specifications==2025.9.1 # via jsonschema, -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==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 mdurl==0.1.2 # via markdown-it-py
oauthlib==3.3.1 # via requests-oauthlib, -r test_requirements.in oauthlib==3.3.1 # via requests-oauthlib, -r test_requirements.in
packaging==26.2 # via pytest, -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 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==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-asyncio==1.3.0 # via -r test_requirements.in
pytest-cov==7.1.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 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==2.34.2 # via apprise, requests-oauthlib, -r test_requirements.in
requests-oauthlib==2.0.0 # via apprise, -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 rpds-py==0.30.0 # via jsonschema, referencing, -r test_requirements.in
ruamel-yaml>0.15.0 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 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.app
import borgmatic.actions.browse.panels import borgmatic.actions.browse.panels
@@ -7,10 +5,6 @@ import pytest
from flexmock import flexmock 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(): async def test_browse_app_with_multiple_configs_uses_configuration_files_list():
app = borgmatic.actions.browse.app.Browse_app( app = borgmatic.actions.browse.app.Browse_app(
configs={ configs={
@@ -1,19 +1,182 @@
import asyncio
import borgmatic.actions.browse.app import borgmatic.actions.browse.app
import borgmatic.actions.browse.carousel import borgmatic.actions.browse.carousel
import borgmatic.actions.browse.loading
import borgmatic.actions.browse.logs import borgmatic.actions.browse.logs
import borgmatic.actions.browse.panels import borgmatic.actions.browse.panels
import borgmatic.actions.browse.workers import borgmatic.actions.browse.workers
import pytest import pytest
import pytest_asyncio
from flexmock import flexmock from flexmock import flexmock
import textual.widgets.option_list import textual.widgets.option_list
from borgmatic.actions.browse import carousel as module
pytestmark = pytest.mark.asyncio(loop_scope='module') def test_make_next_panel_with_configuration_files_list_returns_repositories_list():
loop: asyncio.AbstractEventLoop 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(): 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') 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( app = borgmatic.actions.browse.app.Browse_app(
configs={ configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]}, '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] 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( app = borgmatic.actions.browse.app.Browse_app(
configs={ configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]}, '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] 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(): async def test_carousel_next_action_and_previous_action_returns_to_original_panel():
app = borgmatic.actions.browse.app.Browse_app( app = borgmatic.actions.browse.app.Browse_app(
configs={ configs={