Several browse action unit and integration tests.

This commit is contained in:
Dan Helfman
2026-05-21 23:23:19 -07:00
parent 9935d0ef8d
commit c4170bb126
12 changed files with 403 additions and 19 deletions
+23 -7
View File
@@ -7,6 +7,10 @@ import borgmatic.actions.browse.logs
class Browse_app(textual.app.App):
'''
The main app / entry point for the browse action UI.
'''
BINDINGS = (
textual.binding.Binding(key='q', action='quit', description='quit'),
textual.binding.Binding(key='v', action='toggle_logs', description='view logs'),
@@ -36,6 +40,14 @@ class Browse_app(textual.app.App):
super().__init__()
def compose(self):
'''
Compose a UI consisting of:
* a header with the application name
* a carousel container that contains the main UI panels
* a logs panel where Python logs show up (panel hidden by default)
* a footer with available keys listed
'''
yield textual.widgets.Header()
yield borgmatic.actions.browse.carousel.Carousel(
[borgmatic.actions.browse.panels.Configuration_files_list(self.configs)]
@@ -45,17 +57,21 @@ class Browse_app(textual.app.App):
]
)
logs_widget = borgmatic.actions.browse.panels.Logs()
yield logs_widget
logs_panel = borgmatic.actions.browse.panels.Logs()
yield logs_panel
yield textual.widgets.Footer()
borgmatic.actions.browse.logs.log_to_widget(logs_widget)
borgmatic.actions.browse.logs.log_to_widget(logs_panel)
def on_mount(self):
'''
Set the application title, which ends up in the header.
'''
self.title = 'borgmatic browse'
def action_toggle_logs(self):
logs_container = self.query_one('#logs')
logs_container.styles.display = (
'none' if logs_container.styles.display == 'block' else 'block'
)
'''
Toggle the show/hide status of the logs panel.
'''
logs_panel = self.query_one('#logs')
logs_panel.styles.display = 'none' if logs_panel.styles.display == 'block' else 'block'
+14 -6
View File
@@ -62,6 +62,9 @@ class Carousel(textual.containers.Horizontal):
super().__init__()
def compose(self):
'''
Compose with each of the contained panels and focus the first one.
'''
yield from self.panels
self.focused_panel.focus()
@@ -85,17 +88,22 @@ class Carousel(textual.containers.Horizontal):
'''
Hide the current focused panel and create the next one.
'''
self.focused_panel.styles.display = 'none'
next_panel_index = self.panels.index(self.focused_panel) + 1
if next_panel_index < len(self.panels):
self.focused_panel = self.panels[next_panel_index]
self.focused_panel.styles.display = 'block'
next_panel = self.panels[next_panel_index]
next_panel.styles.display = 'block'
else:
self.focused_panel = make_next_panel(self.focused_panel, option_id)
self.panels.append(self.focused_panel)
self.focused_panel.highlighted = 0
next_panel = make_next_panel(self.focused_panel, option_id)
if next_panel is None:
return
self.panels.append(next_panel)
next_panel.highlighted = 0
self.focused_panel.styles.display = 'none'
self.focused_panel = next_panel
self.focused_panel.focus()
self.refresh(recompose=True)
+1 -1
View File
@@ -31,7 +31,7 @@ class Configuration_files_list(textual.widgets.OptionList):
super().__init__(
*(
textual.widgets.option_list.Option(f'{unexpanded_path}', id=config_path)
textual.widgets.option_list.Option(unexpanded_path, id=config_path)
for config_path in configs
for unexpanded_path in (config_path.replace(home_directory, '~'),)
),
+3 -1
View File
@@ -7,7 +7,9 @@ def run_browse(
configs,
):
'''
Run the "browse" action for the given repository.
Run the "browse" action for the given borgmatic configurations. This launches a console UI.
Raise ValueError if the Textual library (a prerequisite for this action) can't be imported.
'''
if not configs:
return
+6 -3
View File
@@ -1131,8 +1131,9 @@ properties:
- info
- break-lock
- key
- borg
- diff
- browse
- borg
description: |
List of one or more actions to skip running for this configuration
file, even if specified on the command-line (explicitly or
@@ -1348,8 +1349,9 @@ properties:
- info
- break-lock
- key
- borg
- diff
- browse
- borg
description: |
List of actions for which the commands will be
run. Defaults to running for all actions.
@@ -1414,8 +1416,9 @@ properties:
- info
- break-lock
- key
- borg
- diff
- browse
- borg
description: |
Only trigger the hook when borgmatic is run with
particular actions listed here. Defaults to
+1 -1
View File
@@ -46,7 +46,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
@@ -18,6 +18,7 @@ packaging==26.2 # via pytest, -r test_requirements.in
pluggy==1.6.0 # via pytest, pytest-cov, -r test_requirements.in
pygments==2.20.0 # via pytest, -r test_requirements.in
pytest==9.0.3 # via pytest-cov, pytest-timeout, -r test_requirements.in
pytest-asyncio===1.3.0
pytest-cov==7.1.0 # via -r test_requirements.in
pytest-timeout==2.4.0 # via -r test_requirements.in
pyyaml>5.0.0
@@ -0,0 +1,60 @@
import borgmatic.actions.browse.app
import borgmatic.actions.browse.panels
async def test_browse_app_with_multiple_configs_uses_configuration_files_list():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
async with app.run_test() as pilot:
header = app.query_one(selector='Header')
header.name == 'borgmatic browse'
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 1
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Configuration_files_list
)
assert carousel.panels[0].configs == app.configs
app.query_one(selector='Logs')
app.query_one(selector='Footer')
async def test_browse_app_with_one_config_uses_repositories_list():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
}
)
async with app.run_test() as pilot:
header = app.query_one(selector='Header')
header.name == 'borgmatic browse'
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].config == app.configs['test1.yaml']
app.query_one(selector='Logs')
app.query_one(selector='Footer')
async def test_browse_app_key_toggles_logs_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
}
)
async with app.run_test() as pilot:
logs_panel = app.query_one('#logs')
await pilot.press('v')
await pilot.pause()
assert logs_panel.styles.display == 'block'
@@ -0,0 +1,265 @@
import borgmatic.actions.browse.app
import borgmatic.actions.browse.carousel
import borgmatic.actions.browse.panels
import borgmatic.actions.browse.workers
from flexmock import flexmock
import textual.widgets.option_list
async def test_carousel_previous_action_with_multiple_configs_does_not_raise():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
async with app.run_test() as pilot:
await pilot.press('left')
async def test_carousel_previous_action_with_one_config_does_not_raise():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
}
)
async with app.run_test() as pilot:
await pilot.press('left')
async def test_carousel_next_action_with_multiple_configs_advances_panels():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
async with app.run_test() as pilot:
await pilot.press('enter')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 2
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Configuration_files_list
)
assert carousel.panels[0].styles.display == 'none'
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.panels.Repositories_list
)
assert carousel.panels[1].styles.display == 'block'
assert carousel.panels[1].highlighted == 0
assert app.focused == carousel.panels[1]
async def test_carousel_next_action_with_one_config_advances_to_next_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
}
)
flexmock(borgmatic.actions.browse.workers).should_receive('add_repository_archives')
async with app.run_test() as pilot:
await pilot.press('enter')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 2
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Repositories_list
)
assert carousel.panels[0].styles.display == 'none'
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.panels.Archives_list
)
assert carousel.panels[1].styles.display == 'block'
assert carousel.panels[1].highlighted == 0
assert app.focused == carousel.panels[1]
async def test_carousel_next_action_and_previous_action_returns_to_original_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
async with app.run_test() as pilot:
await pilot.press('enter')
await pilot.press('left')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 2
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Configuration_files_list
)
assert carousel.panels[0].styles.display == 'block'
assert carousel.panels[0].highlighted == 0
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.panels.Repositories_list
)
assert carousel.panels[1].styles.display == 'none'
assert app.focused == carousel.panels[0]
async def test_carousel_next_action_and_previous_action_and_next_action_reuses_next_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
flexmock(borgmatic.actions.browse.carousel).should_call('make_next_panel').once()
async with app.run_test() as pilot:
await pilot.press('enter')
await pilot.press('left')
await pilot.press('enter')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 2
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Configuration_files_list
)
assert carousel.panels[0].styles.display == 'none'
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.panels.Repositories_list
)
assert carousel.panels[1].styles.display == 'block'
assert carousel.panels[1].highlighted == 0
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'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
flexmock(borgmatic.actions.browse.carousel).should_receive('make_next_panel').and_return(None)
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.Configuration_files_list
)
assert carousel.panels[0].styles.display == 'block'
assert carousel.panels[0].highlighted == 0
assert app.focused == carousel.panels[0]
async def test_carousel_next_action_and_previous_action_and_down_truncates_next_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
async with app.run_test() as pilot:
await pilot.press('enter')
await pilot.press('left')
await pilot.press('down')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 1
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Configuration_files_list
)
assert carousel.panels[0].styles.display == 'block'
assert carousel.panels[0].highlighted == 1
assert app.focused == carousel.panels[0]
async def test_carousel_down_does_not_raise():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
async with app.run_test() as pilot:
await pilot.press('down')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 1
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Configuration_files_list
)
assert carousel.panels[0].styles.display == 'block'
assert carousel.panels[0].highlighted == 1
assert app.focused == carousel.panels[0]
async def test_carousel_up_does_not_raise():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
async with app.run_test() as pilot:
await pilot.press('down')
carousel = app.query_one(selector='Carousel')
assert len(carousel.panels) == 1
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Configuration_files_list
)
assert carousel.panels[0].styles.display == 'block'
assert carousel.panels[0].highlighted == 1
assert app.focused == carousel.panels[0]
async def test_carousel_next_action_and_select_dot_dot_returns_to_original_panel():
app = borgmatic.actions.browse.app.Browse_app(
configs={
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
}
)
async with app.run_test() as pilot:
await pilot.press('enter')
carousel = app.query_one(selector='Carousel')
carousel.panels[1].options[0] = textual.widgets.option_list.Option('..', id='..')
await pilot.press('enter')
assert len(carousel.panels) == 2
assert isinstance(
carousel.panels[0], borgmatic.actions.browse.panels.Configuration_files_list
)
assert carousel.panels[0].styles.display == 'block'
assert carousel.panels[0].highlighted == 0
assert isinstance(
carousel.panels[1], borgmatic.actions.browse.panels.Repositories_list
)
assert carousel.panels[1].styles.display == 'none'
assert app.focused == carousel.panels[0]
+29
View File
@@ -0,0 +1,29 @@
from flexmock import flexmock
from borgmatic.actions.browse import run as module
import borgmatic.actions.browse.app
def test_run_browse_without_configs_bails():
app = flexmock()
app.should_receive('run').never()
flexmock(borgmatic.actions.browse.app).should_receive('Browse_app').and_return(app)
module.run_browse(
diff_arguments=flexmock(),
global_arguments=flexmock(),
configs=(),
)
def test_run_browse_with_configs_does_not_raise():
flexmock(borgmatic.actions.browse.app).should_receive('Browse_app').and_return(
flexmock(run=lambda: None)
)
module.run_browse(
diff_arguments=flexmock(),
global_arguments=flexmock(),
configs=(flexmock(), flexmock()),
)