mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 02:03:01 +02:00
Several browse action unit and integration tests.
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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, '~'),)
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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()),
|
||||
)
|
||||
Reference in New Issue
Block a user